mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Add namespaces to dynamic tools
This commit is contained in:
@@ -515,6 +515,12 @@
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
"callId": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -24,4 +30,4 @@
|
||||
],
|
||||
"title": "DynamicToolCallParams",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3003,6 +3003,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -417,6 +417,12 @@
|
||||
"callId": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1951,6 +1951,12 @@
|
||||
"callId": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7278,6 +7284,12 @@
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -13572,6 +13584,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/DynamicToolCallStatus"
|
||||
},
|
||||
@@ -16060,4 +16078,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocol",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3875,6 +3875,12 @@
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11416,6 +11422,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -854,6 +854,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -854,6 +854,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -997,6 +997,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1511,6 +1511,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1273,6 +1273,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1273,6 +1273,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1273,6 +1273,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1511,6 +1511,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1273,6 +1273,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -70,6 +70,12 @@
|
||||
"inputSchema": true,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1511,6 +1511,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1273,6 +1273,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -997,6 +997,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -1273,6 +1273,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -997,6 +997,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -997,6 +997,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -997,6 +997,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/DynamicToolCallStatus"
|
||||
},
|
||||
|
||||
@@ -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 DynamicToolCallParams = { threadId: string, turnId: string, callId: string, tool: string, arguments: JsonValue, };
|
||||
export type DynamicToolCallParams = { threadId: string, turnId: string, callId: string, namespace: string | null, tool: string, arguments: JsonValue, };
|
||||
|
||||
@@ -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 = { name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
|
||||
export type DynamicToolSpec = { namespace?: string, name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
|
||||
|
||||
@@ -57,7 +57,7 @@ durationMs: number | null, } | { "type": "fileChange", id: string, changes: Arra
|
||||
/**
|
||||
* The duration of the MCP tool call in milliseconds.
|
||||
*/
|
||||
durationMs: number | null, } | { "type": "dynamicToolCall", id: string, tool: string, arguments: JsonValue, status: DynamicToolCallStatus, contentItems: Array<DynamicToolCallOutputContentItem> | null, success: boolean | null,
|
||||
durationMs: number | null, } | { "type": "dynamicToolCall", id: string, namespace: string | null, tool: string, arguments: JsonValue, status: DynamicToolCallStatus, contentItems: Array<DynamicToolCallOutputContentItem> | null, success: boolean | null,
|
||||
/**
|
||||
* The duration of the dynamic tool call in milliseconds.
|
||||
*/
|
||||
|
||||
@@ -468,6 +468,7 @@ impl ThreadHistoryBuilder {
|
||||
) {
|
||||
let item = ThreadItem::DynamicToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
namespace: payload.namespace.clone(),
|
||||
tool: payload.tool.clone(),
|
||||
arguments: payload.arguments.clone(),
|
||||
status: DynamicToolCallStatus::InProgress,
|
||||
@@ -491,6 +492,7 @@ impl ThreadHistoryBuilder {
|
||||
let duration_ms = i64::try_from(payload.duration.as_millis()).ok();
|
||||
let item = ThreadItem::DynamicToolCall {
|
||||
id: payload.call_id.clone(),
|
||||
namespace: payload.namespace.clone(),
|
||||
tool: payload.tool.clone(),
|
||||
arguments: payload.arguments.clone(),
|
||||
status,
|
||||
@@ -1973,6 +1975,7 @@ mod tests {
|
||||
codex_protocol::dynamic_tools::DynamicToolCallRequest {
|
||||
call_id: "dyn-1".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
namespace: Some("codex_app".into()),
|
||||
tool: "lookup_ticket".into(),
|
||||
arguments: serde_json::json!({"id":"ABC-123"}),
|
||||
},
|
||||
@@ -1980,6 +1983,7 @@ mod tests {
|
||||
EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
|
||||
call_id: "dyn-1".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
namespace: Some("codex_app".into()),
|
||||
tool: "lookup_ticket".into(),
|
||||
arguments: serde_json::json!({"id":"ABC-123"}),
|
||||
content_items: vec![CoreDynamicToolCallOutputContentItem::InputText {
|
||||
@@ -2002,6 +2006,7 @@ mod tests {
|
||||
turns[0].items[1],
|
||||
ThreadItem::DynamicToolCall {
|
||||
id: "dyn-1".into(),
|
||||
namespace: Some("codex_app".into()),
|
||||
tool: "lookup_ticket".into(),
|
||||
arguments: serde_json::json!({"id":"ABC-123"}),
|
||||
status: DynamicToolCallStatus::Completed,
|
||||
|
||||
@@ -608,6 +608,8 @@ pub struct ToolsV2 {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DynamicToolSpec {
|
||||
#[ts(optional)]
|
||||
pub namespace: Option<String>,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
@@ -618,6 +620,7 @@ pub struct DynamicToolSpec {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DynamicToolSpecDe {
|
||||
namespace: Option<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
input_schema: JsonValue,
|
||||
@@ -631,6 +634,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let DynamicToolSpecDe {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
@@ -639,6 +643,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
} = DynamicToolSpecDe::deserialize(deserializer)?;
|
||||
|
||||
Ok(Self {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
@@ -4680,6 +4685,7 @@ pub enum ThreadItem {
|
||||
#[ts(rename_all = "camelCase")]
|
||||
DynamicToolCall {
|
||||
id: String,
|
||||
namespace: Option<String>,
|
||||
tool: String,
|
||||
arguments: JsonValue,
|
||||
status: DynamicToolCallStatus,
|
||||
@@ -6405,6 +6411,7 @@ pub struct DynamicToolCallParams {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub call_id: String,
|
||||
pub namespace: Option<String>,
|
||||
pub tool: String,
|
||||
pub arguments: JsonValue,
|
||||
}
|
||||
@@ -8783,6 +8790,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
actual,
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -937,10 +937,12 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
let call_id = request.call_id;
|
||||
let turn_id = request.turn_id;
|
||||
let namespace = request.namespace;
|
||||
let tool = request.tool;
|
||||
let arguments = request.arguments;
|
||||
let item = ThreadItem::DynamicToolCall {
|
||||
id: call_id.clone(),
|
||||
namespace: namespace.clone(),
|
||||
tool: tool.clone(),
|
||||
arguments: arguments.clone(),
|
||||
status: DynamicToolCallStatus::InProgress,
|
||||
@@ -960,6 +962,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
namespace,
|
||||
tool: tool.clone(),
|
||||
arguments: arguments.clone(),
|
||||
};
|
||||
@@ -998,6 +1001,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
let duration_ms = i64::try_from(response.duration.as_millis()).ok();
|
||||
let item = ThreadItem::DynamicToolCall {
|
||||
id: response.call_id,
|
||||
namespace: response.namespace,
|
||||
tool: response.tool,
|
||||
arguments: response.arguments,
|
||||
status,
|
||||
|
||||
@@ -2539,6 +2539,7 @@ impl CodexMessageProcessor {
|
||||
dynamic_tools
|
||||
.into_iter()
|
||||
.map(|tool| CoreDynamicToolSpec {
|
||||
namespace: tool.namespace,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
@@ -9208,6 +9209,29 @@ fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
if !seen.insert(name.to_string()) {
|
||||
return Err(format!("duplicate dynamic tool name: {name}"));
|
||||
}
|
||||
let namespace = tool.namespace.as_deref().map(str::trim);
|
||||
if let Some(namespace) = namespace {
|
||||
if namespace.is_empty() {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace must not be empty for {name}"
|
||||
));
|
||||
}
|
||||
if Some(namespace) != tool.namespace.as_deref() {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace has leading/trailing whitespace for {name}: {namespace}",
|
||||
));
|
||||
}
|
||||
if namespace == "mcp" || namespace.starts_with("mcp__") {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace is reserved for {name}: {namespace}"
|
||||
));
|
||||
}
|
||||
}
|
||||
if tool.defer_loading && namespace.is_none() {
|
||||
return Err(format!(
|
||||
"deferred dynamic tool must include a namespace: {name}"
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(err) = codex_tools::parse_tool_input_schema(&tool.input_schema) {
|
||||
return Err(format!(
|
||||
@@ -10160,6 +10184,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_unsupported_input_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({"type": "null"}),
|
||||
@@ -10172,6 +10197,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_sanitizable_input_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
// Missing `type` is common; core sanitizes these to a supported schema.
|
||||
@@ -10184,6 +10210,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_nullable_field_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -10199,6 +10226,76 @@ mod tests {
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_requires_namespace_for_deferred_tools() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("missing namespace");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
assert!(err.contains("namespace"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_namespace_for_deferred_tools() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_empty_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("empty namespace");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
assert!(err.contains("namespace"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_reserved_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("mcp__server__".to_string()),
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("reserved namespace");
|
||||
assert!(err.contains("my_tool"), "unexpected error: {err}");
|
||||
assert!(err.contains("reserved"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_from_stored_thread_preserves_millisecond_precision() {
|
||||
let created_at =
|
||||
|
||||
@@ -1067,6 +1067,7 @@ mod tests {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "call-0".to_string(),
|
||||
namespace: None,
|
||||
tool: "tool".to_string(),
|
||||
arguments: json!({}),
|
||||
},
|
||||
@@ -1124,6 +1125,7 @@ mod tests {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "call-0".to_string(),
|
||||
namespace: None,
|
||||
tool: "tool".to_string(),
|
||||
arguments: json!({}),
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "demo_tool".to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
@@ -136,6 +137,7 @@ async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Resu
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "hidden_tool".to_string(),
|
||||
description: "Hidden dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -222,6 +224,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: tool_name.to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -273,6 +276,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
assert_eq!(started.turn_id, turn_id.clone());
|
||||
let ThreadItem::DynamicToolCall {
|
||||
id,
|
||||
namespace,
|
||||
tool,
|
||||
arguments,
|
||||
status,
|
||||
@@ -284,6 +288,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
assert_eq!(id, call_id);
|
||||
assert_eq!(namespace, None);
|
||||
assert_eq!(tool, tool_name);
|
||||
assert_eq!(arguments, tool_args);
|
||||
assert_eq!(status, DynamicToolCallStatus::InProgress);
|
||||
@@ -306,6 +311,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
thread_id: thread_id.clone(),
|
||||
turn_id: turn_id.clone(),
|
||||
call_id: call_id.to_string(),
|
||||
namespace: None,
|
||||
tool: tool_name.to_string(),
|
||||
arguments: tool_args.clone(),
|
||||
};
|
||||
@@ -326,6 +332,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
assert_eq!(completed.turn_id, turn_id);
|
||||
let ThreadItem::DynamicToolCall {
|
||||
id,
|
||||
namespace,
|
||||
tool,
|
||||
arguments,
|
||||
status,
|
||||
@@ -337,6 +344,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
assert_eq!(id, call_id);
|
||||
assert_eq!(namespace, None);
|
||||
assert_eq!(tool, tool_name);
|
||||
assert_eq!(arguments, tool_args);
|
||||
assert_eq!(status, DynamicToolCallStatus::Completed);
|
||||
@@ -391,6 +399,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: tool_name.to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -454,6 +463,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
thread_id,
|
||||
turn_id: turn_id.clone(),
|
||||
call_id: call_id.to_string(),
|
||||
namespace: None,
|
||||
tool: tool_name.to_string(),
|
||||
arguments: tool_args,
|
||||
};
|
||||
|
||||
@@ -127,6 +127,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> {
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
dynamic_tools: Some(vec![DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: tool_name.to_string(),
|
||||
description: "Deterministic wait tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -194,6 +195,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> {
|
||||
thread_id: thread_id.clone(),
|
||||
turn_id: started.turn_id,
|
||||
call_id: call_id.to_string(),
|
||||
namespace: None,
|
||||
tool: tool_name.to_string(),
|
||||
arguments: tool_args,
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ use codex_protocol::protocol::ReasoningRawContentDeltaEvent;
|
||||
use codex_protocol::protocol::TurnDiffEvent;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::filter_tool_suggest_discoverable_tools_for_client;
|
||||
use codex_utils_stream_parser::AssistantTextChunk;
|
||||
use codex_utils_stream_parser::AssistantTextStreamParser;
|
||||
@@ -952,7 +954,7 @@ pub(crate) fn build_prompt(
|
||||
.dynamic_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.defer_loading)
|
||||
.map(|tool| tool.name.as_str())
|
||||
.map(|tool| ToolName::new(tool.namespace.clone(), tool.name.clone()))
|
||||
.collect::<HashSet<_>>();
|
||||
let tools = if deferred_dynamic_tools.is_empty() {
|
||||
router.model_visible_specs()
|
||||
@@ -960,7 +962,7 @@ pub(crate) fn build_prompt(
|
||||
router
|
||||
.model_visible_specs()
|
||||
.into_iter()
|
||||
.filter(|spec| !deferred_dynamic_tools.contains(spec.name()))
|
||||
.filter_map(|spec| filter_deferred_dynamic_tool_spec(spec, &deferred_dynamic_tools))
|
||||
.collect()
|
||||
};
|
||||
|
||||
@@ -974,6 +976,35 @@ pub(crate) fn build_prompt(
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_deferred_dynamic_tool_spec(
|
||||
spec: ToolSpec,
|
||||
deferred_dynamic_tools: &HashSet<ToolName>,
|
||||
) -> Option<ToolSpec> {
|
||||
match spec {
|
||||
ToolSpec::Function(tool) => {
|
||||
if deferred_dynamic_tools.contains(&ToolName::plain(tool.name.as_str())) {
|
||||
None
|
||||
} else {
|
||||
Some(ToolSpec::Function(tool))
|
||||
}
|
||||
}
|
||||
ToolSpec::Namespace(mut namespace) => {
|
||||
let namespace_name = namespace.name.clone();
|
||||
namespace.tools.retain(|tool| match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) => !deferred_dynamic_tools.contains(
|
||||
&ToolName::namespaced(namespace_name.as_str(), tool.name.as_str()),
|
||||
),
|
||||
});
|
||||
if namespace.tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ToolSpec::Namespace(namespace))
|
||||
}
|
||||
}
|
||||
spec => Some(spec),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(level = "trace",
|
||||
skip_all,
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_tools::ToolName;
|
||||
use serde_json::Value;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -50,14 +51,13 @@ impl ToolHandler for DynamicToolHandler {
|
||||
};
|
||||
|
||||
let args: Value = parse_arguments(&arguments)?;
|
||||
let response =
|
||||
request_dynamic_tool(&session, turn.as_ref(), call_id, tool_name.display(), args)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"dynamic tool call was cancelled before receiving a response".to_string(),
|
||||
)
|
||||
})?;
|
||||
let response = request_dynamic_tool(&session, turn.as_ref(), call_id, tool_name, args)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"dynamic tool call was cancelled before receiving a response".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let DynamicToolResponse {
|
||||
content_items,
|
||||
@@ -75,9 +75,11 @@ async fn request_dynamic_tool(
|
||||
session: &Session,
|
||||
turn_context: &TurnContext,
|
||||
call_id: String,
|
||||
tool: String,
|
||||
tool_name: ToolName,
|
||||
arguments: Value,
|
||||
) -> Option<DynamicToolResponse> {
|
||||
let namespace = tool_name.namespace;
|
||||
let tool = tool_name.name;
|
||||
let turn_id = turn_context.sub_id.clone();
|
||||
let (tx_response, rx_response) = oneshot::channel();
|
||||
let event_id = call_id.clone();
|
||||
@@ -99,6 +101,7 @@ async fn request_dynamic_tool(
|
||||
let event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest {
|
||||
call_id: call_id.clone(),
|
||||
turn_id: turn_id.clone(),
|
||||
namespace: namespace.clone(),
|
||||
tool: tool.clone(),
|
||||
arguments: arguments.clone(),
|
||||
});
|
||||
@@ -109,6 +112,7 @@ async fn request_dynamic_tool(
|
||||
Some(response) => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
|
||||
call_id,
|
||||
turn_id,
|
||||
namespace,
|
||||
tool,
|
||||
arguments,
|
||||
content_items: response.content_items.clone(),
|
||||
@@ -119,6 +123,7 @@ async fn request_dynamic_tool(
|
||||
None => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
|
||||
call_id,
|
||||
turn_id,
|
||||
namespace,
|
||||
tool,
|
||||
arguments,
|
||||
content_items: Vec::new(),
|
||||
|
||||
@@ -209,6 +209,7 @@ mod tests {
|
||||
#[test]
|
||||
fn mixed_search_results_coalesce_mcp_namespaces() {
|
||||
let dynamic_tools = vec![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!({
|
||||
@@ -277,21 +278,25 @@ mod tests {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
ToolSearchOutputTool::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,
|
||||
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: "codex_app".to_string(),
|
||||
description: "Tools provided by the current Codex thread.".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,
|
||||
})],
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1587,7 +1587,11 @@ impl JsReplManager {
|
||||
ResponsesApiNamespaceTool::Function(tool) => {
|
||||
let tool_name =
|
||||
ToolName::namespaced(namespace.name.clone(), tool.name.clone());
|
||||
(tool_name.display() == req.tool_name).then_some(tool_name)
|
||||
let code_mode_name =
|
||||
codex_tools::code_mode_name_for_tool_name(&tool_name);
|
||||
(code_mode_name == req.tool_name
|
||||
|| tool_name.display() == req.tool_name)
|
||||
.then_some(tool_name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1821,6 +1821,7 @@ async fn js_repl_emit_image_rejects_mixed_content() -> anyhow::Result<()> {
|
||||
|
||||
let (session, turn, rx_event) =
|
||||
make_session_and_context_with_dynamic_tools_and_rx(vec![DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "inline_image".to_string(),
|
||||
description: "Returns inline text and image content.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
@@ -1920,6 +1921,7 @@ async fn js_repl_dynamic_tool_response_preserves_js_line_separator_text() -> any
|
||||
] {
|
||||
let (session, turn, rx_event) =
|
||||
make_session_and_context_with_dynamic_tools_and_rx(vec![DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: tool_name.to_string(),
|
||||
description: description.to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
@@ -1995,6 +1997,7 @@ async fn js_repl_can_call_hidden_dynamic_tools() -> anyhow::Result<()> {
|
||||
|
||||
let (session, turn, rx_event) =
|
||||
make_session_and_context_with_dynamic_tools_and_rx(vec![DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "hidden_dynamic_tool".to_string(),
|
||||
description: "A hidden dynamic tool.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_tools::ResponsesApiNamespace;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolSearchOutputTool;
|
||||
use codex_tools::ToolSearchResultSource;
|
||||
use codex_tools::dynamic_tool_to_responses_api_tool;
|
||||
@@ -68,9 +70,19 @@ fn mcp_tool_search_entry(info: &ToolInfo) -> Result<ToolSearchEntry, serde_json:
|
||||
}
|
||||
|
||||
fn dynamic_tool_search_entry(tool: &DynamicToolSpec) -> Result<ToolSearchEntry, serde_json::Error> {
|
||||
let output_tool = dynamic_tool_to_responses_api_tool(tool)?;
|
||||
let output = match tool.namespace.as_ref() {
|
||||
Some(namespace) => ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: namespace.clone(),
|
||||
description: "Tools provided by the current Codex thread.".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],
|
||||
}),
|
||||
None => ToolSearchOutputTool::Function(output_tool),
|
||||
};
|
||||
|
||||
Ok(ToolSearchEntry {
|
||||
search_text: build_dynamic_search_text(tool),
|
||||
output: ToolSearchOutputTool::Function(dynamic_tool_to_responses_api_tool(tool)?),
|
||||
output,
|
||||
limit_bucket: None,
|
||||
})
|
||||
}
|
||||
@@ -135,6 +147,10 @@ fn build_dynamic_search_text(tool: &DynamicToolSpec) -> String {
|
||||
tool.description.clone(),
|
||||
];
|
||||
|
||||
if let Some(namespace) = &tool.namespace {
|
||||
parts.push(namespace.clone());
|
||||
}
|
||||
|
||||
parts.extend(
|
||||
tool.input_schema
|
||||
.get("properties")
|
||||
|
||||
@@ -2555,6 +2555,7 @@ async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> {
|
||||
.start_thread_with_tools(
|
||||
base_test.config.clone(),
|
||||
vec![DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "hidden_dynamic_tool".to_string(),
|
||||
description: "A hidden dynamic tool.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
@@ -2635,6 +2636,7 @@ text(
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(request.namespace.as_deref(), Some("codex_app"));
|
||||
assert_eq!(request.tool, "hidden_dynamic_tool");
|
||||
assert_eq!(request.arguments, serde_json::json!({ "city": "Paris" }));
|
||||
test.codex
|
||||
|
||||
@@ -731,6 +731,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
"item": {
|
||||
"type": "function_call",
|
||||
"call_id": dynamic_call_id,
|
||||
"namespace": "codex_app",
|
||||
"name": tool_name,
|
||||
"arguments": tool_call_arguments,
|
||||
}
|
||||
@@ -755,6 +756,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: tool_name.to_string(),
|
||||
description: tool_description.to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
@@ -794,6 +796,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
unreachable!("event guard guarantees DynamicToolCallRequest");
|
||||
};
|
||||
assert_eq!(request.call_id, dynamic_call_id);
|
||||
assert_eq!(request.namespace.as_deref(), Some("codex_app"));
|
||||
assert_eq!(request.tool, tool_name);
|
||||
assert_eq!(request.arguments, tool_args);
|
||||
|
||||
@@ -833,12 +836,17 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
assert_eq!(
|
||||
tools,
|
||||
vec![json!({
|
||||
"type": "function",
|
||||
"name": tool_name,
|
||||
"description": tool_description,
|
||||
"strict": false,
|
||||
"defer_loading": true,
|
||||
"parameters": input_schema,
|
||||
"type": "namespace",
|
||||
"name": "codex_app",
|
||||
"description": "Tools provided by the current Codex thread.",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": tool_name,
|
||||
"description": tool_description,
|
||||
"strict": false,
|
||||
"defer_loading": true,
|
||||
"parameters": input_schema,
|
||||
}],
|
||||
})]
|
||||
);
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "geo_lookup".to_string(),
|
||||
description: "lookup a city".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -113,6 +114,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
defer_loading: true,
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "weather_lookup".to_string(),
|
||||
description: "lookup weather".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -8,6 +8,8 @@ use ts_rs::TS;
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamicToolSpec {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
@@ -20,6 +22,8 @@ pub struct DynamicToolSpec {
|
||||
pub struct DynamicToolCallRequest {
|
||||
pub call_id: String,
|
||||
pub turn_id: String,
|
||||
#[serde(default)]
|
||||
pub namespace: Option<String>,
|
||||
pub tool: String,
|
||||
pub arguments: JsonValue,
|
||||
}
|
||||
@@ -44,6 +48,7 @@ pub enum DynamicToolCallOutputContentItem {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DynamicToolSpecDe {
|
||||
namespace: Option<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
input_schema: JsonValue,
|
||||
@@ -57,6 +62,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let DynamicToolSpecDe {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
@@ -65,6 +71,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
} = DynamicToolSpecDe::deserialize(deserializer)?;
|
||||
|
||||
Ok(Self {
|
||||
namespace,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
@@ -99,6 +106,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
actual,
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -2433,6 +2433,9 @@ pub struct DynamicToolCallResponseEvent {
|
||||
pub call_id: String,
|
||||
/// Turn ID that this dynamic tool call belongs to.
|
||||
pub turn_id: String,
|
||||
/// Dynamic tool namespace, when one was provided.
|
||||
#[serde(default)]
|
||||
pub namespace: Option<String>,
|
||||
/// Dynamic tool name.
|
||||
pub tool: String,
|
||||
/// Dynamic tool call arguments.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE thread_dynamic_tools
|
||||
ADD COLUMN namespace TEXT;
|
||||
@@ -56,7 +56,7 @@ WHERE threads.id = ?
|
||||
) -> anyhow::Result<Option<Vec<DynamicToolSpec>>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT name, description, input_schema, defer_loading
|
||||
SELECT namespace, name, description, input_schema, defer_loading
|
||||
FROM thread_dynamic_tools
|
||||
WHERE thread_id = ?
|
||||
ORDER BY position ASC
|
||||
@@ -73,6 +73,7 @@ ORDER BY position ASC
|
||||
let input_schema: String = row.try_get("input_schema")?;
|
||||
let input_schema = serde_json::from_str::<Value>(input_schema.as_str())?;
|
||||
tools.push(DynamicToolSpec {
|
||||
namespace: row.try_get("namespace")?,
|
||||
name: row.try_get("name")?,
|
||||
description: row.try_get("description")?,
|
||||
input_schema,
|
||||
@@ -767,16 +768,18 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
INSERT INTO thread_dynamic_tools (
|
||||
thread_id,
|
||||
position,
|
||||
namespace,
|
||||
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.name.as_str())
|
||||
.bind(tool.description.as_str())
|
||||
.bind(input_schema)
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn augment_tool_spec_for_code_mode(spec: ToolSpec) -> ToolSpec {
|
||||
let tool_name =
|
||||
ToolName::namespaced(namespace.name.clone(), tool.name.clone());
|
||||
let definition = CodeModeToolDefinition {
|
||||
name: tool_name.display(),
|
||||
name: code_mode_name_for_tool_name(&tool_name),
|
||||
tool_name,
|
||||
description: tool.description.clone(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
@@ -208,7 +208,7 @@ fn code_mode_tool_definitions_for_spec(spec: &ToolSpec) -> Vec<CodeModeToolDefin
|
||||
ResponsesApiNamespaceTool::Function(tool) => {
|
||||
let tool_name = ToolName::namespaced(namespace.name.clone(), tool.name.clone());
|
||||
CodeModeToolDefinition {
|
||||
name: tool_name.display(),
|
||||
name: code_mode_name_for_tool_name(&tool_name),
|
||||
tool_name,
|
||||
description: tool.description.clone(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
@@ -225,6 +225,18 @@ fn code_mode_tool_definitions_for_spec(spec: &ToolSpec) -> Vec<CodeModeToolDefin
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code_mode_name_for_tool_name(tool_name: &ToolName) -> String {
|
||||
if tool_name
|
||||
.namespace
|
||||
.as_deref()
|
||||
.is_some_and(|namespace| namespace.starts_with("mcp__"))
|
||||
{
|
||||
tool_name.display()
|
||||
} else {
|
||||
tool_name.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "code_mode_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
|
||||
pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serde_json::Error> {
|
||||
let DynamicToolSpec {
|
||||
namespace: _,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::collections::BTreeMap;
|
||||
#[test]
|
||||
fn parse_dynamic_tool_sanitizes_input_schema() {
|
||||
let tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
@@ -42,6 +43,7 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
|
||||
#[test]
|
||||
fn parse_dynamic_tool_preserves_defer_loading() {
|
||||
let tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
|
||||
@@ -44,6 +44,7 @@ pub use apply_patch_tool::ApplyPatchToolArgs;
|
||||
pub use apply_patch_tool::create_apply_patch_freeform_tool;
|
||||
pub use apply_patch_tool::create_apply_patch_json_tool;
|
||||
pub use code_mode::augment_tool_spec_for_code_mode;
|
||||
pub use code_mode::code_mode_name_for_tool_name;
|
||||
pub use code_mode::collect_code_mode_exec_prompt_tool_definitions;
|
||||
pub use code_mode::collect_code_mode_tool_definitions;
|
||||
pub use code_mode::create_code_mode_tool;
|
||||
|
||||
@@ -51,6 +51,7 @@ fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
|
||||
#[test]
|
||||
fn dynamic_tool_to_responses_api_tool_preserves_defer_loading() {
|
||||
let tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup_order".to_string(),
|
||||
description: "Look up an order".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use crate::TOOL_SEARCH_TOOL_NAME;
|
||||
use crate::TOOL_SUGGEST_TOOL_NAME;
|
||||
use crate::ToolHandlerKind;
|
||||
use crate::ToolName;
|
||||
use crate::ToolRegistryPlan;
|
||||
use crate::ToolRegistryPlanParams;
|
||||
use crate::ToolSearchSource;
|
||||
@@ -558,12 +559,22 @@ pub fn build_tool_registry_plan(
|
||||
for tool in params.dynamic_tools {
|
||||
match dynamic_tool_to_responses_api_tool(tool) {
|
||||
Ok(converted_tool) => {
|
||||
let handler_name = ToolName::new(tool.namespace.clone(), tool.name.clone());
|
||||
let spec = if let Some(namespace) = tool.namespace.as_ref() {
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: namespace.clone(),
|
||||
description: default_namespace_description(namespace),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(converted_tool)],
|
||||
})
|
||||
} else {
|
||||
ToolSpec::Function(converted_tool)
|
||||
};
|
||||
plan.push_spec(
|
||||
ToolSpec::Function(converted_tool),
|
||||
spec,
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler(tool.name.clone(), ToolHandlerKind::DynamicTool);
|
||||
plan.register_handler(handler_name, ToolHandlerKind::DynamicTool);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
|
||||
@@ -1424,6 +1424,7 @@ fn search_tool_registers_for_deferred_dynamic_tools() {
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create, update, view, or delete recurring automations.".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -1447,13 +1448,15 @@ fn search_tool_registers_for_deferred_dynamic_tools() {
|
||||
panic!("expected tool_search tool");
|
||||
};
|
||||
assert!(description.contains("- Dynamic tools: Tools provided by the current Codex thread."));
|
||||
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME, "automation_update"]);
|
||||
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME, "codex_app"]);
|
||||
let dynamic_tool = find_namespace_function_tool(&tools, "codex_app", "automation_update");
|
||||
assert_eq!(dynamic_tool.defer_loading, Some(true));
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::plain(TOOL_SEARCH_TOOL_NAME),
|
||||
kind: ToolHandlerKind::ToolSearch,
|
||||
}));
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::plain("automation_update"),
|
||||
name: ToolName::namespaced("codex_app", "automation_update"),
|
||||
kind: ToolHandlerKind::DynamicTool,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -653,6 +653,7 @@ mod tests {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "tool-1".to_string(),
|
||||
namespace: None,
|
||||
tool: "tool".to_string(),
|
||||
arguments: json!({}),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user