2025-08-05 19:27:52 -07:00
|
|
|
|
use serde::Deserialize;
|
2025-05-30 14:07:03 -07:00
|
|
|
|
use serde::Serialize;
|
2025-08-11 03:57:39 +03:00
|
|
|
|
use serde_json::Value as JsonValue;
|
2025-05-30 14:07:03 -07:00
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
use std::collections::BTreeMap;
|
2025-08-05 19:27:52 -07:00
|
|
|
|
use std::collections::HashMap;
|
2025-05-30 14:07:03 -07:00
|
|
|
|
|
2025-08-04 23:50:03 -07:00
|
|
|
|
use crate::model_family::ModelFamily;
|
2025-07-29 11:22:02 -07:00
|
|
|
|
use crate::plan_tool::PLAN_TOOL;
|
2025-08-05 20:44:20 -07:00
|
|
|
|
use crate::protocol::AskForApproval;
|
|
|
|
|
|
use crate::protocol::SandboxPolicy;
|
2025-08-22 13:42:34 -07:00
|
|
|
|
use crate::tool_apply_patch::ApplyPatchToolType;
|
|
|
|
|
|
use crate::tool_apply_patch::create_apply_patch_freeform_tool;
|
|
|
|
|
|
use crate::tool_apply_patch::create_apply_patch_json_tool;
|
2025-05-30 14:07:03 -07:00
|
|
|
|
|
2025-08-05 19:27:52 -07:00
|
|
|
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
|
|
|
|
|
pub struct ResponsesApiTool {
|
|
|
|
|
|
pub(crate) name: String,
|
|
|
|
|
|
pub(crate) description: String,
|
|
|
|
|
|
/// TODO: Validation. When strict is set to true, the JSON schema,
|
|
|
|
|
|
/// `required` and `additional_properties` must be present. All fields in
|
|
|
|
|
|
/// `properties` must be present in `required`.
|
2025-07-29 11:22:02 -07:00
|
|
|
|
pub(crate) strict: bool,
|
|
|
|
|
|
pub(crate) parameters: JsonSchema,
|
2025-05-30 14:07:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-22 13:42:34 -07:00
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
|
|
|
|
pub struct FreeformTool {
|
|
|
|
|
|
pub(crate) name: String,
|
|
|
|
|
|
pub(crate) description: String,
|
|
|
|
|
|
pub(crate) format: FreeformToolFormat,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
|
|
|
|
pub struct FreeformToolFormat {
|
|
|
|
|
|
pub(crate) r#type: String,
|
|
|
|
|
|
pub(crate) syntax: String,
|
|
|
|
|
|
pub(crate) definition: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-30 14:07:03 -07:00
|
|
|
|
/// When serialized as JSON, this produces a valid "Tool" in the OpenAI
|
|
|
|
|
|
/// Responses API.
|
2025-08-05 19:27:52 -07:00
|
|
|
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
2025-05-30 14:07:03 -07:00
|
|
|
|
#[serde(tag = "type")]
|
|
|
|
|
|
pub(crate) enum OpenAiTool {
|
|
|
|
|
|
#[serde(rename = "function")]
|
|
|
|
|
|
Function(ResponsesApiTool),
|
|
|
|
|
|
#[serde(rename = "local_shell")]
|
|
|
|
|
|
LocalShell {},
|
2025-08-28 19:24:38 -07:00
|
|
|
|
// TODO: Understand why we get an error on web_search although the API docs say it's supported.
|
|
|
|
|
|
// https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#:~:text=%7B%20type%3A%20%22web_search%22%20%7D%2C
|
2025-09-03 01:16:47 -07:00
|
|
|
|
#[serde(rename = "web_search")]
|
2025-08-23 22:58:56 -07:00
|
|
|
|
WebSearch {},
|
2025-08-22 13:42:34 -07:00
|
|
|
|
#[serde(rename = "custom")]
|
|
|
|
|
|
Freeform(FreeformTool),
|
2025-05-30 14:07:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 19:27:52 -07:00
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub enum ConfigShellToolType {
|
|
|
|
|
|
DefaultShell,
|
2025-08-05 20:44:20 -07:00
|
|
|
|
ShellWithRequest { sandbox_policy: SandboxPolicy },
|
2025-08-05 19:27:52 -07:00
|
|
|
|
LocalShell,
|
2025-08-22 18:10:55 -07:00
|
|
|
|
StreamableShell,
|
2025-08-05 19:27:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
2025-08-24 22:43:42 -07:00
|
|
|
|
pub(crate) struct ToolsConfig {
|
2025-08-05 19:27:52 -07:00
|
|
|
|
pub shell_type: ConfigShellToolType,
|
|
|
|
|
|
pub plan_tool: bool,
|
2025-08-22 13:42:34 -07:00
|
|
|
|
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
2025-08-23 22:58:56 -07:00
|
|
|
|
pub web_search_request: bool,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
pub include_view_image_tool: bool,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
pub experimental_unified_exec_tool: bool,
|
2025-08-05 19:27:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-24 22:43:42 -07:00
|
|
|
|
pub(crate) struct ToolsConfigParams<'a> {
|
|
|
|
|
|
pub(crate) model_family: &'a ModelFamily,
|
|
|
|
|
|
pub(crate) approval_policy: AskForApproval,
|
|
|
|
|
|
pub(crate) sandbox_policy: SandboxPolicy,
|
|
|
|
|
|
pub(crate) include_plan_tool: bool,
|
|
|
|
|
|
pub(crate) include_apply_patch_tool: bool,
|
|
|
|
|
|
pub(crate) include_web_search_request: bool,
|
|
|
|
|
|
pub(crate) use_streamable_shell_tool: bool,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
pub(crate) include_view_image_tool: bool,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
pub(crate) experimental_unified_exec_tool: bool,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 19:27:52 -07:00
|
|
|
|
impl ToolsConfig {
|
2025-08-24 22:43:42 -07:00
|
|
|
|
pub fn new(params: &ToolsConfigParams) -> Self {
|
|
|
|
|
|
let ToolsConfigParams {
|
|
|
|
|
|
model_family,
|
|
|
|
|
|
approval_policy,
|
|
|
|
|
|
sandbox_policy,
|
|
|
|
|
|
include_plan_tool,
|
|
|
|
|
|
include_apply_patch_tool,
|
|
|
|
|
|
include_web_search_request,
|
|
|
|
|
|
use_streamable_shell_tool,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
} = params;
|
|
|
|
|
|
let mut shell_type = if *use_streamable_shell_tool {
|
2025-08-22 18:10:55 -07:00
|
|
|
|
ConfigShellToolType::StreamableShell
|
|
|
|
|
|
} else if model_family.uses_local_shell_tool {
|
2025-08-05 19:27:52 -07:00
|
|
|
|
ConfigShellToolType::LocalShell
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ConfigShellToolType::DefaultShell
|
|
|
|
|
|
};
|
2025-08-22 18:10:55 -07:00
|
|
|
|
if matches!(approval_policy, AskForApproval::OnRequest) && !use_streamable_shell_tool {
|
2025-08-05 20:44:20 -07:00
|
|
|
|
shell_type = ConfigShellToolType::ShellWithRequest {
|
|
|
|
|
|
sandbox_policy: sandbox_policy.clone(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-05 19:27:52 -07:00
|
|
|
|
|
2025-08-22 13:42:34 -07:00
|
|
|
|
let apply_patch_tool_type = match model_family.apply_patch_tool_type {
|
|
|
|
|
|
Some(ApplyPatchToolType::Freeform) => Some(ApplyPatchToolType::Freeform),
|
|
|
|
|
|
Some(ApplyPatchToolType::Function) => Some(ApplyPatchToolType::Function),
|
|
|
|
|
|
None => {
|
2025-08-24 22:43:42 -07:00
|
|
|
|
if *include_apply_patch_tool {
|
2025-08-22 13:42:34 -07:00
|
|
|
|
Some(ApplyPatchToolType::Freeform)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-05 19:27:52 -07:00
|
|
|
|
Self {
|
|
|
|
|
|
shell_type,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
plan_tool: *include_plan_tool,
|
2025-08-22 13:42:34 -07:00
|
|
|
|
apply_patch_tool_type,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
web_search_request: *include_web_search_request,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: *include_view_image_tool,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: *experimental_unified_exec_tool,
|
2025-08-05 19:27:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-30 14:07:03 -07:00
|
|
|
|
/// Generic JSON‑Schema subset needed for our tool definitions
|
2025-08-05 19:27:52 -07:00
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2025-05-30 14:07:03 -07:00
|
|
|
|
#[serde(tag = "type", rename_all = "lowercase")]
|
|
|
|
|
|
pub(crate) enum JsonSchema {
|
2025-08-05 20:44:20 -07:00
|
|
|
|
Boolean {
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
description: Option<String>,
|
|
|
|
|
|
},
|
|
|
|
|
|
String {
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
description: Option<String>,
|
|
|
|
|
|
},
|
2025-08-11 03:57:39 +03:00
|
|
|
|
/// MCP schema allows "number" | "integer" for Number
|
|
|
|
|
|
#[serde(alias = "integer")]
|
2025-08-05 20:44:20 -07:00
|
|
|
|
Number {
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
description: Option<String>,
|
|
|
|
|
|
},
|
2025-05-30 14:07:03 -07:00
|
|
|
|
Array {
|
|
|
|
|
|
items: Box<JsonSchema>,
|
2025-08-05 20:44:20 -07:00
|
|
|
|
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
description: Option<String>,
|
2025-05-30 14:07:03 -07:00
|
|
|
|
},
|
|
|
|
|
|
Object {
|
|
|
|
|
|
properties: BTreeMap<String, JsonSchema>,
|
2025-08-05 19:27:52 -07:00
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
required: Option<Vec<String>>,
|
|
|
|
|
|
#[serde(
|
|
|
|
|
|
rename = "additionalProperties",
|
|
|
|
|
|
skip_serializing_if = "Option::is_none"
|
|
|
|
|
|
)]
|
|
|
|
|
|
additional_properties: Option<bool>,
|
2025-05-30 14:07:03 -07:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 20:44:20 -07:00
|
|
|
|
fn create_shell_tool() -> OpenAiTool {
|
2025-05-30 14:07:03 -07:00
|
|
|
|
let mut properties = BTreeMap::new();
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"command".to_string(),
|
|
|
|
|
|
JsonSchema::Array {
|
2025-08-05 20:44:20 -07:00
|
|
|
|
items: Box::new(JsonSchema::String { description: None }),
|
2025-08-21 19:58:07 -07:00
|
|
|
|
description: Some("The command to execute".to_string()),
|
2025-05-30 14:07:03 -07:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-08-05 20:44:20 -07:00
|
|
|
|
properties.insert(
|
|
|
|
|
|
"workdir".to_string(),
|
2025-08-21 19:58:07 -07:00
|
|
|
|
JsonSchema::String {
|
|
|
|
|
|
description: Some("The working directory to execute the command in".to_string()),
|
|
|
|
|
|
},
|
2025-08-05 20:44:20 -07:00
|
|
|
|
);
|
|
|
|
|
|
properties.insert(
|
2025-08-21 19:58:07 -07:00
|
|
|
|
"timeout_ms".to_string(),
|
|
|
|
|
|
JsonSchema::Number {
|
|
|
|
|
|
description: Some("The timeout for the command in milliseconds".to_string()),
|
|
|
|
|
|
},
|
2025-08-05 20:44:20 -07:00
|
|
|
|
);
|
2025-05-30 14:07:03 -07:00
|
|
|
|
|
2025-08-04 23:50:03 -07:00
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
2025-08-05 19:27:52 -07:00
|
|
|
|
name: "shell".to_string(),
|
|
|
|
|
|
description: "Runs a shell command and returns its output".to_string(),
|
2025-05-30 14:07:03 -07:00
|
|
|
|
strict: false,
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties,
|
2025-08-05 19:27:52 -07:00
|
|
|
|
required: Some(vec!["command".to_string()]),
|
|
|
|
|
|
additional_properties: Some(false),
|
2025-05-30 14:07:03 -07:00
|
|
|
|
},
|
2025-08-04 23:50:03 -07:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-05-30 14:07:03 -07:00
|
|
|
|
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
fn create_unified_exec_tool() -> OpenAiTool {
|
|
|
|
|
|
let mut properties = BTreeMap::new();
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"input".to_string(),
|
|
|
|
|
|
JsonSchema::Array {
|
|
|
|
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
|
|
|
|
description: Some(
|
|
|
|
|
|
"When no session_id is provided, treat the array as the command and arguments \
|
|
|
|
|
|
to launch. When session_id is set, concatenate the strings (in order) and write \
|
|
|
|
|
|
them to the session's stdin."
|
|
|
|
|
|
.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"session_id".to_string(),
|
|
|
|
|
|
JsonSchema::String {
|
|
|
|
|
|
description: Some(
|
|
|
|
|
|
"Identifier for an existing interactive session. If omitted, a new command \
|
|
|
|
|
|
is spawned."
|
|
|
|
|
|
.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"timeout_ms".to_string(),
|
|
|
|
|
|
JsonSchema::Number {
|
|
|
|
|
|
description: Some(
|
|
|
|
|
|
"Maximum time in milliseconds to wait for output after writing the input."
|
|
|
|
|
|
.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "unified_exec".to_string(),
|
|
|
|
|
|
description:
|
|
|
|
|
|
"Runs a command in a PTY. Provide a session_id to reuse an existing interactive session.".to_string(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties,
|
|
|
|
|
|
required: Some(vec!["input".to_string()]),
|
|
|
|
|
|
additional_properties: Some(false),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 20:44:20 -07:00
|
|
|
|
fn create_shell_tool_for_sandbox(sandbox_policy: &SandboxPolicy) -> OpenAiTool {
|
|
|
|
|
|
let mut properties = BTreeMap::new();
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"command".to_string(),
|
|
|
|
|
|
JsonSchema::Array {
|
|
|
|
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
|
|
|
|
description: Some("The command to execute".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"workdir".to_string(),
|
|
|
|
|
|
JsonSchema::String {
|
|
|
|
|
|
description: Some("The working directory to execute the command in".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
properties.insert(
|
2025-08-21 19:58:07 -07:00
|
|
|
|
"timeout_ms".to_string(),
|
2025-08-05 20:44:20 -07:00
|
|
|
|
JsonSchema::Number {
|
|
|
|
|
|
description: Some("The timeout for the command in milliseconds".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"with_escalated_permissions".to_string(),
|
|
|
|
|
|
JsonSchema::Boolean {
|
|
|
|
|
|
description: Some("Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"justification".to_string(),
|
|
|
|
|
|
JsonSchema::String {
|
2025-08-21 20:07:41 -07:00
|
|
|
|
description: Some("Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.".to_string()),
|
2025-08-05 20:44:20 -07:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "shell".to_string(),
|
2025-09-12 14:24:09 -04:00
|
|
|
|
description: "Runs a shell command and returns its output.".to_string(),
|
2025-08-05 20:44:20 -07:00
|
|
|
|
strict: false,
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties,
|
|
|
|
|
|
required: Some(vec!["command".to_string()]),
|
|
|
|
|
|
additional_properties: Some(false),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-08-27 17:41:23 -07:00
|
|
|
|
|
|
|
|
|
|
fn create_view_image_tool() -> OpenAiTool {
|
|
|
|
|
|
// Support only local filesystem path.
|
|
|
|
|
|
let mut properties = BTreeMap::new();
|
|
|
|
|
|
properties.insert(
|
|
|
|
|
|
"path".to_string(),
|
|
|
|
|
|
JsonSchema::String {
|
|
|
|
|
|
description: Some("Local filesystem path to an image file".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "view_image".to_string(),
|
|
|
|
|
|
description:
|
|
|
|
|
|
"Attach a local image (by filesystem path) to the conversation context for this turn."
|
|
|
|
|
|
.to_string(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties,
|
|
|
|
|
|
required: Some(vec!["path".to_string()]),
|
|
|
|
|
|
additional_properties: Some(false),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-08-22 13:42:34 -07:00
|
|
|
|
/// TODO(dylan): deprecate once we get rid of json tool
|
2025-08-15 11:55:53 -04:00
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
|
pub(crate) struct ApplyPatchToolArgs {
|
|
|
|
|
|
pub(crate) input: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-30 14:07:03 -07:00
|
|
|
|
/// Returns JSON values that are compatible with Function Calling in the
|
|
|
|
|
|
/// Responses API:
|
|
|
|
|
|
/// https://platform.openai.com/docs/guides/function-calling?api-mode=responses
|
2025-08-21 20:07:41 -07:00
|
|
|
|
pub fn create_tools_json_for_responses_api(
|
2025-08-05 19:27:52 -07:00
|
|
|
|
tools: &Vec<OpenAiTool>,
|
2025-05-30 14:07:03 -07:00
|
|
|
|
) -> crate::error::Result<Vec<serde_json::Value>> {
|
2025-08-05 19:27:52 -07:00
|
|
|
|
let mut tools_json = Vec::new();
|
2025-08-04 23:50:03 -07:00
|
|
|
|
|
2025-08-05 19:27:52 -07:00
|
|
|
|
for tool in tools {
|
2025-08-28 19:24:38 -07:00
|
|
|
|
let json = serde_json::to_value(tool)?;
|
|
|
|
|
|
tools_json.push(json);
|
2025-05-30 14:07:03 -07:00
|
|
|
|
}
|
2025-07-29 11:22:02 -07:00
|
|
|
|
|
2025-05-30 14:07:03 -07:00
|
|
|
|
Ok(tools_json)
|
|
|
|
|
|
}
|
|
|
|
|
|
/// Returns JSON values that are compatible with Function Calling in the
|
|
|
|
|
|
/// Chat Completions API:
|
|
|
|
|
|
/// https://platform.openai.com/docs/guides/function-calling?api-mode=chat
|
|
|
|
|
|
pub(crate) fn create_tools_json_for_chat_completions_api(
|
2025-08-05 19:27:52 -07:00
|
|
|
|
tools: &Vec<OpenAiTool>,
|
2025-05-30 14:07:03 -07:00
|
|
|
|
) -> crate::error::Result<Vec<serde_json::Value>> {
|
|
|
|
|
|
// We start with the JSON for the Responses API and than rewrite it to match
|
|
|
|
|
|
// the chat completions tool call format.
|
2025-08-05 19:27:52 -07:00
|
|
|
|
let responses_api_tools_json = create_tools_json_for_responses_api(tools)?;
|
2025-05-30 14:07:03 -07:00
|
|
|
|
let tools_json = responses_api_tools_json
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.filter_map(|mut tool| {
|
|
|
|
|
|
if tool.get("type") != Some(&serde_json::Value::String("function".to_string())) {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(map) = tool.as_object_mut() {
|
|
|
|
|
|
// Remove "type" field as it is not needed in chat completions.
|
|
|
|
|
|
map.remove("type");
|
|
|
|
|
|
Some(json!({
|
|
|
|
|
|
"type": "function",
|
|
|
|
|
|
"function": map,
|
|
|
|
|
|
}))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect::<Vec<serde_json::Value>>();
|
|
|
|
|
|
Ok(tools_json)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 19:27:52 -07:00
|
|
|
|
pub(crate) fn mcp_tool_to_openai_tool(
|
2025-05-30 14:07:03 -07:00
|
|
|
|
fully_qualified_name: String,
|
|
|
|
|
|
tool: mcp_types::Tool,
|
2025-08-05 19:27:52 -07:00
|
|
|
|
) -> Result<ResponsesApiTool, serde_json::Error> {
|
2025-05-30 14:07:03 -07:00
|
|
|
|
let mcp_types::Tool {
|
|
|
|
|
|
description,
|
|
|
|
|
|
mut input_schema,
|
|
|
|
|
|
..
|
|
|
|
|
|
} = tool;
|
|
|
|
|
|
|
|
|
|
|
|
// OpenAI models mandate the "properties" field in the schema. The Agents
|
|
|
|
|
|
// SDK fixed this by inserting an empty object for "properties" if it is not
|
|
|
|
|
|
// already present https://github.com/openai/openai-agents-python/issues/449
|
|
|
|
|
|
// so here we do the same.
|
|
|
|
|
|
if input_schema.properties.is_none() {
|
|
|
|
|
|
input_schema.properties = Some(serde_json::Value::Object(serde_json::Map::new()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-11 03:57:39 +03:00
|
|
|
|
// Serialize to a raw JSON value so we can sanitize schemas coming from MCP
|
|
|
|
|
|
// servers. Some servers omit the top-level or nested `type` in JSON
|
|
|
|
|
|
// Schemas (e.g. using enum/anyOf), or use unsupported variants like
|
|
|
|
|
|
// `integer`. Our internal JsonSchema is a small subset and requires
|
|
|
|
|
|
// `type`, so we coerce/sanitize here for compatibility.
|
|
|
|
|
|
let mut serialized_input_schema = serde_json::to_value(input_schema)?;
|
|
|
|
|
|
sanitize_json_schema(&mut serialized_input_schema);
|
2025-08-05 19:27:52 -07:00
|
|
|
|
let input_schema = serde_json::from_value::<JsonSchema>(serialized_input_schema)?;
|
|
|
|
|
|
|
|
|
|
|
|
Ok(ResponsesApiTool {
|
|
|
|
|
|
name: fully_qualified_name,
|
|
|
|
|
|
description: description.unwrap_or_default(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
parameters: input_schema,
|
2025-05-30 14:07:03 -07:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-08-05 19:27:52 -07:00
|
|
|
|
|
2025-08-11 03:57:39 +03:00
|
|
|
|
/// Sanitize a JSON Schema (as serde_json::Value) so it can fit our limited
|
|
|
|
|
|
/// JsonSchema enum. This function:
|
|
|
|
|
|
/// - Ensures every schema object has a "type". If missing, infers it from
|
|
|
|
|
|
/// common keywords (properties => object, items => array, enum/const/format => string)
|
|
|
|
|
|
/// and otherwise defaults to "string".
|
|
|
|
|
|
/// - Fills required child fields (e.g. array items, object properties) with
|
|
|
|
|
|
/// permissive defaults when absent.
|
|
|
|
|
|
fn sanitize_json_schema(value: &mut JsonValue) {
|
|
|
|
|
|
match value {
|
|
|
|
|
|
JsonValue::Bool(_) => {
|
|
|
|
|
|
// JSON Schema boolean form: true/false. Coerce to an accept-all string.
|
|
|
|
|
|
*value = json!({ "type": "string" });
|
|
|
|
|
|
}
|
|
|
|
|
|
JsonValue::Array(arr) => {
|
|
|
|
|
|
for v in arr.iter_mut() {
|
|
|
|
|
|
sanitize_json_schema(v);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
JsonValue::Object(map) => {
|
|
|
|
|
|
// First, recursively sanitize known nested schema holders
|
2025-08-19 13:22:02 -07:00
|
|
|
|
if let Some(props) = map.get_mut("properties")
|
|
|
|
|
|
&& let Some(props_map) = props.as_object_mut()
|
|
|
|
|
|
{
|
|
|
|
|
|
for (_k, v) in props_map.iter_mut() {
|
|
|
|
|
|
sanitize_json_schema(v);
|
2025-08-11 03:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(items) = map.get_mut("items") {
|
|
|
|
|
|
sanitize_json_schema(items);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Some schemas use oneOf/anyOf/allOf - sanitize their entries
|
|
|
|
|
|
for combiner in ["oneOf", "anyOf", "allOf", "prefixItems"] {
|
|
|
|
|
|
if let Some(v) = map.get_mut(combiner) {
|
|
|
|
|
|
sanitize_json_schema(v);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Normalize/ensure type
|
|
|
|
|
|
let mut ty = map
|
|
|
|
|
|
.get("type")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.map(|s| s.to_string());
|
|
|
|
|
|
|
|
|
|
|
|
// If type is an array (union), pick first supported; else leave to inference
|
2025-08-19 13:22:02 -07:00
|
|
|
|
if ty.is_none()
|
|
|
|
|
|
&& let Some(JsonValue::Array(types)) = map.get("type")
|
|
|
|
|
|
{
|
|
|
|
|
|
for t in types {
|
|
|
|
|
|
if let Some(tt) = t.as_str()
|
|
|
|
|
|
&& matches!(
|
|
|
|
|
|
tt,
|
|
|
|
|
|
"object" | "array" | "string" | "number" | "integer" | "boolean"
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
ty = Some(tt.to_string());
|
|
|
|
|
|
break;
|
2025-08-11 03:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Infer type if still missing
|
|
|
|
|
|
if ty.is_none() {
|
|
|
|
|
|
if map.contains_key("properties")
|
|
|
|
|
|
|| map.contains_key("required")
|
|
|
|
|
|
|| map.contains_key("additionalProperties")
|
|
|
|
|
|
{
|
|
|
|
|
|
ty = Some("object".to_string());
|
|
|
|
|
|
} else if map.contains_key("items") || map.contains_key("prefixItems") {
|
|
|
|
|
|
ty = Some("array".to_string());
|
|
|
|
|
|
} else if map.contains_key("enum")
|
|
|
|
|
|
|| map.contains_key("const")
|
|
|
|
|
|
|| map.contains_key("format")
|
|
|
|
|
|
{
|
|
|
|
|
|
ty = Some("string".to_string());
|
|
|
|
|
|
} else if map.contains_key("minimum")
|
|
|
|
|
|
|| map.contains_key("maximum")
|
|
|
|
|
|
|| map.contains_key("exclusiveMinimum")
|
|
|
|
|
|
|| map.contains_key("exclusiveMaximum")
|
|
|
|
|
|
|| map.contains_key("multipleOf")
|
|
|
|
|
|
{
|
|
|
|
|
|
ty = Some("number".to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// If we still couldn't infer, default to string
|
|
|
|
|
|
let ty = ty.unwrap_or_else(|| "string".to_string());
|
|
|
|
|
|
map.insert("type".to_string(), JsonValue::String(ty.to_string()));
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure object schemas have properties map
|
|
|
|
|
|
if ty == "object" {
|
|
|
|
|
|
if !map.contains_key("properties") {
|
|
|
|
|
|
map.insert(
|
|
|
|
|
|
"properties".to_string(),
|
|
|
|
|
|
JsonValue::Object(serde_json::Map::new()),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
// If additionalProperties is an object schema, sanitize it too.
|
|
|
|
|
|
// Leave booleans as-is, since JSON Schema allows boolean here.
|
|
|
|
|
|
if let Some(ap) = map.get_mut("additionalProperties") {
|
|
|
|
|
|
let is_bool = matches!(ap, JsonValue::Bool(_));
|
|
|
|
|
|
if !is_bool {
|
|
|
|
|
|
sanitize_json_schema(ap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure array schemas have items
|
|
|
|
|
|
if ty == "array" && !map.contains_key("items") {
|
|
|
|
|
|
map.insert("items".to_string(), json!({ "type": "string" }));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 19:27:52 -07:00
|
|
|
|
/// Returns a list of OpenAiTools based on the provided config and MCP tools.
|
|
|
|
|
|
/// Note that the keys of mcp_tools should be fully qualified names. See
|
|
|
|
|
|
/// [`McpConnectionManager`] for more details.
|
|
|
|
|
|
pub(crate) fn get_openai_tools(
|
|
|
|
|
|
config: &ToolsConfig,
|
|
|
|
|
|
mcp_tools: Option<HashMap<String, mcp_types::Tool>>,
|
|
|
|
|
|
) -> Vec<OpenAiTool> {
|
|
|
|
|
|
let mut tools: Vec<OpenAiTool> = Vec::new();
|
|
|
|
|
|
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
if config.experimental_unified_exec_tool {
|
|
|
|
|
|
tools.push(create_unified_exec_tool());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
match &config.shell_type {
|
|
|
|
|
|
ConfigShellToolType::DefaultShell => {
|
|
|
|
|
|
tools.push(create_shell_tool());
|
|
|
|
|
|
}
|
|
|
|
|
|
ConfigShellToolType::ShellWithRequest { sandbox_policy } => {
|
|
|
|
|
|
tools.push(create_shell_tool_for_sandbox(sandbox_policy));
|
|
|
|
|
|
}
|
|
|
|
|
|
ConfigShellToolType::LocalShell => {
|
|
|
|
|
|
tools.push(OpenAiTool::LocalShell {});
|
|
|
|
|
|
}
|
|
|
|
|
|
ConfigShellToolType::StreamableShell => {
|
|
|
|
|
|
tools.push(OpenAiTool::Function(
|
|
|
|
|
|
crate::exec_command::create_exec_command_tool_for_responses_api(),
|
|
|
|
|
|
));
|
|
|
|
|
|
tools.push(OpenAiTool::Function(
|
|
|
|
|
|
crate::exec_command::create_write_stdin_tool_for_responses_api(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2025-08-22 18:10:55 -07:00
|
|
|
|
}
|
2025-08-05 19:27:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if config.plan_tool {
|
|
|
|
|
|
tools.push(PLAN_TOOL.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-22 13:42:34 -07:00
|
|
|
|
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
|
|
|
|
|
|
match apply_patch_tool_type {
|
|
|
|
|
|
ApplyPatchToolType::Freeform => {
|
|
|
|
|
|
tools.push(create_apply_patch_freeform_tool());
|
|
|
|
|
|
}
|
|
|
|
|
|
ApplyPatchToolType::Function => {
|
|
|
|
|
|
tools.push(create_apply_patch_json_tool());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-15 11:55:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-23 22:58:56 -07:00
|
|
|
|
if config.web_search_request {
|
|
|
|
|
|
tools.push(OpenAiTool::WebSearch {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-27 17:41:23 -07:00
|
|
|
|
// Include the view_image tool so the agent can attach images to context.
|
|
|
|
|
|
if config.include_view_image_tool {
|
|
|
|
|
|
tools.push(create_view_image_tool());
|
|
|
|
|
|
}
|
2025-08-05 19:27:52 -07:00
|
|
|
|
if let Some(mcp_tools) = mcp_tools {
|
Fix cache hit rate by making MCP tools order deterministic (#2611)
Fixes https://github.com/openai/codex/issues/2610
This PR sorts the tools in `get_openai_tools` by name to ensure a
consistent MCP tool order.
Currently, MCP servers are stored in a HashMap, which does not guarantee
ordering. As a result, the tool order changes across turns, effectively
breaking prompt caching in multi-turn sessions.
An alternative solution would be to replace the HashMap with an ordered
structure, but that would require a much larger code change. Given that
it is unrealistic to have so many MCP tools that sorting would cause
performance issues, this lightweight fix is chosen instead.
By ensuring deterministic tool order, this change should significantly
improve cache hit rates and prevent users from hitting usage limits too
quickly. (For reference, my own sessions last week reached the limit
unusually fast, with cache hit rates falling below 1%.)
## Result
After this fix, sessions with MCP servers now show caching behavior
almost identical to sessions without MCP servers.
Without MCP | With MCP
:-------------------------:|:-------------------------:
<img width="1368" height="1634" alt="image"
src="https://github.com/user-attachments/assets/26edab45-7be8-4d6a-b471-558016615fc8"
/> | <img width="1356" height="1632" alt="image"
src="https://github.com/user-attachments/assets/5f3634e0-3888-420b-9aaf-deefd9397b40"
/>
2025-08-25 11:56:24 +09:00
|
|
|
|
// Ensure deterministic ordering to maximize prompt cache hits.
|
|
|
|
|
|
let mut entries: Vec<(String, mcp_types::Tool)> = mcp_tools.into_iter().collect();
|
|
|
|
|
|
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
|
|
|
|
|
|
|
|
|
|
|
for (name, tool) in entries.into_iter() {
|
2025-08-05 19:27:52 -07:00
|
|
|
|
match mcp_tool_to_openai_tool(name.clone(), tool.clone()) {
|
|
|
|
|
|
Ok(converted_tool) => tools.push(OpenAiTool::Function(converted_tool)),
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
tracing::error!("Failed to convert {name:?} MCP tool to OpenAI tool: {e:?}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tools
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use crate::model_family::find_family_for_model;
|
|
|
|
|
|
use mcp_types::ToolInputSchema;
|
2025-08-11 03:57:39 +03:00
|
|
|
|
use pretty_assertions::assert_eq;
|
2025-08-05 19:27:52 -07:00
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
fn assert_eq_tool_names(tools: &[OpenAiTool], expected_names: &[&str]) {
|
|
|
|
|
|
let tool_names = tools
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|tool| match tool {
|
|
|
|
|
|
OpenAiTool::Function(ResponsesApiTool { name, .. }) => name,
|
|
|
|
|
|
OpenAiTool::LocalShell {} => "local_shell",
|
2025-08-23 22:58:56 -07:00
|
|
|
|
OpenAiTool::WebSearch {} => "web_search",
|
2025-08-22 13:42:34 -07:00
|
|
|
|
OpenAiTool::Freeform(FreeformTool { name, .. }) => name,
|
2025-08-05 19:27:52 -07:00
|
|
|
|
})
|
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
tool_names.len(),
|
|
|
|
|
|
expected_names.len(),
|
|
|
|
|
|
"tool_name mismatch, {tool_names:?}, {expected_names:?}",
|
|
|
|
|
|
);
|
|
|
|
|
|
for (name, expected_name) in tool_names.iter().zip(expected_names.iter()) {
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
name, expected_name,
|
|
|
|
|
|
"tool_name mismatch, {name:?}, {expected_name:?}"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_get_openai_tools() {
|
|
|
|
|
|
let model_family = find_family_for_model("codex-mini-latest")
|
|
|
|
|
|
.expect("codex-mini-latest should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: true,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: true,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
2025-08-05 19:27:52 -07:00
|
|
|
|
let tools = get_openai_tools(&config, Some(HashMap::new()));
|
|
|
|
|
|
|
2025-08-27 17:41:23 -07:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
&["unified_exec", "update_plan", "web_search", "view_image"],
|
2025-08-27 17:41:23 -07:00
|
|
|
|
);
|
2025-08-05 19:27:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_get_openai_tools_default_shell() {
|
|
|
|
|
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: true,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: true,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
2025-08-05 19:27:52 -07:00
|
|
|
|
let tools = get_openai_tools(&config, Some(HashMap::new()));
|
|
|
|
|
|
|
2025-08-27 17:41:23 -07:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
&["unified_exec", "update_plan", "web_search", "view_image"],
|
2025-08-27 17:41:23 -07:00
|
|
|
|
);
|
2025-08-05 19:27:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_get_openai_tools_mcp_tools() {
|
|
|
|
|
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: false,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: true,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
2025-08-05 19:27:52 -07:00
|
|
|
|
let tools = get_openai_tools(
|
|
|
|
|
|
&config,
|
|
|
|
|
|
Some(HashMap::from([(
|
|
|
|
|
|
"test_server/do_something_cool".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "do_something_cool".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({
|
|
|
|
|
|
"string_argument": {
|
|
|
|
|
|
"type": "string",
|
|
|
|
|
|
},
|
|
|
|
|
|
"number_argument": {
|
|
|
|
|
|
"type": "number",
|
|
|
|
|
|
},
|
|
|
|
|
|
"object_argument": {
|
|
|
|
|
|
"type": "object",
|
|
|
|
|
|
"properties": {
|
|
|
|
|
|
"string_property": { "type": "string" },
|
|
|
|
|
|
"number_property": { "type": "number" },
|
|
|
|
|
|
},
|
|
|
|
|
|
"required": [
|
2025-08-28 19:24:38 -07:00
|
|
|
|
"string_property",
|
|
|
|
|
|
"number_property",
|
2025-08-05 19:27:52 -07:00
|
|
|
|
],
|
|
|
|
|
|
"additionalProperties": Some(false),
|
|
|
|
|
|
},
|
|
|
|
|
|
})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("Do something cool".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
)])),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-08-23 22:58:56 -07:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
&[
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
"unified_exec",
|
2025-08-27 17:41:23 -07:00
|
|
|
|
"web_search",
|
|
|
|
|
|
"view_image",
|
|
|
|
|
|
"test_server/do_something_cool",
|
|
|
|
|
|
],
|
2025-08-23 22:58:56 -07:00
|
|
|
|
);
|
2025-08-05 19:27:52 -07:00
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
2025-08-27 17:41:23 -07:00
|
|
|
|
tools[3],
|
2025-08-05 19:27:52 -07:00
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "test_server/do_something_cool".to_string(),
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties: BTreeMap::from([
|
2025-08-05 20:44:20 -07:00
|
|
|
|
(
|
|
|
|
|
|
"string_argument".to_string(),
|
|
|
|
|
|
JsonSchema::String { description: None }
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"number_argument".to_string(),
|
|
|
|
|
|
JsonSchema::Number { description: None }
|
|
|
|
|
|
),
|
2025-08-05 19:27:52 -07:00
|
|
|
|
(
|
|
|
|
|
|
"object_argument".to_string(),
|
|
|
|
|
|
JsonSchema::Object {
|
|
|
|
|
|
properties: BTreeMap::from([
|
2025-08-05 20:44:20 -07:00
|
|
|
|
(
|
|
|
|
|
|
"string_property".to_string(),
|
|
|
|
|
|
JsonSchema::String { description: None }
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"number_property".to_string(),
|
|
|
|
|
|
JsonSchema::Number { description: None }
|
|
|
|
|
|
),
|
2025-08-05 19:27:52 -07:00
|
|
|
|
]),
|
|
|
|
|
|
required: Some(vec![
|
|
|
|
|
|
"string_property".to_string(),
|
|
|
|
|
|
"number_property".to_string(),
|
|
|
|
|
|
]),
|
|
|
|
|
|
additional_properties: Some(false),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
]),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
additional_properties: None,
|
|
|
|
|
|
},
|
|
|
|
|
|
description: "Do something cool".to_string(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-08-11 03:57:39 +03:00
|
|
|
|
|
Fix cache hit rate by making MCP tools order deterministic (#2611)
Fixes https://github.com/openai/codex/issues/2610
This PR sorts the tools in `get_openai_tools` by name to ensure a
consistent MCP tool order.
Currently, MCP servers are stored in a HashMap, which does not guarantee
ordering. As a result, the tool order changes across turns, effectively
breaking prompt caching in multi-turn sessions.
An alternative solution would be to replace the HashMap with an ordered
structure, but that would require a much larger code change. Given that
it is unrealistic to have so many MCP tools that sorting would cause
performance issues, this lightweight fix is chosen instead.
By ensuring deterministic tool order, this change should significantly
improve cache hit rates and prevent users from hitting usage limits too
quickly. (For reference, my own sessions last week reached the limit
unusually fast, with cache hit rates falling below 1%.)
## Result
After this fix, sessions with MCP servers now show caching behavior
almost identical to sessions without MCP servers.
Without MCP | With MCP
:-------------------------:|:-------------------------:
<img width="1368" height="1634" alt="image"
src="https://github.com/user-attachments/assets/26edab45-7be8-4d6a-b471-558016615fc8"
/> | <img width="1356" height="1632" alt="image"
src="https://github.com/user-attachments/assets/5f3634e0-3888-420b-9aaf-deefd9397b40"
/>
2025-08-25 11:56:24 +09:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_get_openai_tools_mcp_tools_sorted_by_name() {
|
|
|
|
|
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: false,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: false,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
Fix cache hit rate by making MCP tools order deterministic (#2611)
Fixes https://github.com/openai/codex/issues/2610
This PR sorts the tools in `get_openai_tools` by name to ensure a
consistent MCP tool order.
Currently, MCP servers are stored in a HashMap, which does not guarantee
ordering. As a result, the tool order changes across turns, effectively
breaking prompt caching in multi-turn sessions.
An alternative solution would be to replace the HashMap with an ordered
structure, but that would require a much larger code change. Given that
it is unrealistic to have so many MCP tools that sorting would cause
performance issues, this lightweight fix is chosen instead.
By ensuring deterministic tool order, this change should significantly
improve cache hit rates and prevent users from hitting usage limits too
quickly. (For reference, my own sessions last week reached the limit
unusually fast, with cache hit rates falling below 1%.)
## Result
After this fix, sessions with MCP servers now show caching behavior
almost identical to sessions without MCP servers.
Without MCP | With MCP
:-------------------------:|:-------------------------:
<img width="1368" height="1634" alt="image"
src="https://github.com/user-attachments/assets/26edab45-7be8-4d6a-b471-558016615fc8"
/> | <img width="1356" height="1632" alt="image"
src="https://github.com/user-attachments/assets/5f3634e0-3888-420b-9aaf-deefd9397b40"
/>
2025-08-25 11:56:24 +09:00
|
|
|
|
|
|
|
|
|
|
// Intentionally construct a map with keys that would sort alphabetically.
|
|
|
|
|
|
let tools_map: HashMap<String, mcp_types::Tool> = HashMap::from([
|
|
|
|
|
|
(
|
|
|
|
|
|
"test_server/do".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "a".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("a".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"test_server/something".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "b".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("b".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"test_server/cool".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "c".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("c".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
let tools = get_openai_tools(&config, Some(tools_map));
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
// Expect unified_exec first, followed by MCP tools sorted by fully-qualified name.
|
Fix cache hit rate by making MCP tools order deterministic (#2611)
Fixes https://github.com/openai/codex/issues/2610
This PR sorts the tools in `get_openai_tools` by name to ensure a
consistent MCP tool order.
Currently, MCP servers are stored in a HashMap, which does not guarantee
ordering. As a result, the tool order changes across turns, effectively
breaking prompt caching in multi-turn sessions.
An alternative solution would be to replace the HashMap with an ordered
structure, but that would require a much larger code change. Given that
it is unrealistic to have so many MCP tools that sorting would cause
performance issues, this lightweight fix is chosen instead.
By ensuring deterministic tool order, this change should significantly
improve cache hit rates and prevent users from hitting usage limits too
quickly. (For reference, my own sessions last week reached the limit
unusually fast, with cache hit rates falling below 1%.)
## Result
After this fix, sessions with MCP servers now show caching behavior
almost identical to sessions without MCP servers.
Without MCP | With MCP
:-------------------------:|:-------------------------:
<img width="1368" height="1634" alt="image"
src="https://github.com/user-attachments/assets/26edab45-7be8-4d6a-b471-558016615fc8"
/> | <img width="1356" height="1632" alt="image"
src="https://github.com/user-attachments/assets/5f3634e0-3888-420b-9aaf-deefd9397b40"
/>
2025-08-25 11:56:24 +09:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
|
|
|
|
|
&[
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
"unified_exec",
|
2025-08-27 17:41:23 -07:00
|
|
|
|
"view_image",
|
Fix cache hit rate by making MCP tools order deterministic (#2611)
Fixes https://github.com/openai/codex/issues/2610
This PR sorts the tools in `get_openai_tools` by name to ensure a
consistent MCP tool order.
Currently, MCP servers are stored in a HashMap, which does not guarantee
ordering. As a result, the tool order changes across turns, effectively
breaking prompt caching in multi-turn sessions.
An alternative solution would be to replace the HashMap with an ordered
structure, but that would require a much larger code change. Given that
it is unrealistic to have so many MCP tools that sorting would cause
performance issues, this lightweight fix is chosen instead.
By ensuring deterministic tool order, this change should significantly
improve cache hit rates and prevent users from hitting usage limits too
quickly. (For reference, my own sessions last week reached the limit
unusually fast, with cache hit rates falling below 1%.)
## Result
After this fix, sessions with MCP servers now show caching behavior
almost identical to sessions without MCP servers.
Without MCP | With MCP
:-------------------------:|:-------------------------:
<img width="1368" height="1634" alt="image"
src="https://github.com/user-attachments/assets/26edab45-7be8-4d6a-b471-558016615fc8"
/> | <img width="1356" height="1632" alt="image"
src="https://github.com/user-attachments/assets/5f3634e0-3888-420b-9aaf-deefd9397b40"
/>
2025-08-25 11:56:24 +09:00
|
|
|
|
"test_server/cool",
|
|
|
|
|
|
"test_server/do",
|
|
|
|
|
|
"test_server/something",
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-11 03:57:39 +03:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
|
|
|
|
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: false,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: true,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
2025-08-11 03:57:39 +03:00
|
|
|
|
|
|
|
|
|
|
let tools = get_openai_tools(
|
|
|
|
|
|
&config,
|
|
|
|
|
|
Some(HashMap::from([(
|
|
|
|
|
|
"dash/search".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "search".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({
|
|
|
|
|
|
"query": {
|
|
|
|
|
|
"description": "search query"
|
|
|
|
|
|
}
|
|
|
|
|
|
})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("Search docs".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
)])),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-08-27 17:41:23 -07:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
&["unified_exec", "web_search", "view_image", "dash/search"],
|
2025-08-27 17:41:23 -07:00
|
|
|
|
);
|
2025-08-11 03:57:39 +03:00
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
2025-08-27 17:41:23 -07:00
|
|
|
|
tools[3],
|
2025-08-11 03:57:39 +03:00
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "dash/search".to_string(),
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties: BTreeMap::from([(
|
|
|
|
|
|
"query".to_string(),
|
|
|
|
|
|
JsonSchema::String {
|
|
|
|
|
|
description: Some("search query".to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
)]),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
additional_properties: None,
|
|
|
|
|
|
},
|
|
|
|
|
|
description: "Search docs".to_string(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_mcp_tool_integer_normalized_to_number() {
|
|
|
|
|
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: false,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: true,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
2025-08-11 03:57:39 +03:00
|
|
|
|
|
|
|
|
|
|
let tools = get_openai_tools(
|
|
|
|
|
|
&config,
|
|
|
|
|
|
Some(HashMap::from([(
|
|
|
|
|
|
"dash/paginate".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "paginate".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({
|
|
|
|
|
|
"page": { "type": "integer" }
|
|
|
|
|
|
})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("Pagination".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
)])),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-08-27 17:41:23 -07:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
&["unified_exec", "web_search", "view_image", "dash/paginate"],
|
2025-08-27 17:41:23 -07:00
|
|
|
|
);
|
2025-08-11 03:57:39 +03:00
|
|
|
|
assert_eq!(
|
2025-08-27 17:41:23 -07:00
|
|
|
|
tools[3],
|
2025-08-11 03:57:39 +03:00
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "dash/paginate".to_string(),
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties: BTreeMap::from([(
|
|
|
|
|
|
"page".to_string(),
|
|
|
|
|
|
JsonSchema::Number { description: None }
|
|
|
|
|
|
)]),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
additional_properties: None,
|
|
|
|
|
|
},
|
|
|
|
|
|
description: "Pagination".to_string(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
|
|
|
|
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: false,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: true,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
2025-08-11 03:57:39 +03:00
|
|
|
|
|
|
|
|
|
|
let tools = get_openai_tools(
|
|
|
|
|
|
&config,
|
|
|
|
|
|
Some(HashMap::from([(
|
|
|
|
|
|
"dash/tags".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "tags".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({
|
|
|
|
|
|
"tags": { "type": "array" }
|
|
|
|
|
|
})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("Tags".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
)])),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
|
|
|
|
|
&["unified_exec", "web_search", "view_image", "dash/tags"],
|
|
|
|
|
|
);
|
2025-08-11 03:57:39 +03:00
|
|
|
|
assert_eq!(
|
2025-08-27 17:41:23 -07:00
|
|
|
|
tools[3],
|
2025-08-11 03:57:39 +03:00
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "dash/tags".to_string(),
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties: BTreeMap::from([(
|
|
|
|
|
|
"tags".to_string(),
|
|
|
|
|
|
JsonSchema::Array {
|
|
|
|
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
|
|
|
|
description: None
|
|
|
|
|
|
}
|
|
|
|
|
|
)]),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
additional_properties: None,
|
|
|
|
|
|
},
|
|
|
|
|
|
description: "Tags".to_string(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_mcp_tool_anyof_defaults_to_string() {
|
|
|
|
|
|
let model_family = find_family_for_model("o3").expect("o3 should be a valid model family");
|
2025-08-24 22:43:42 -07:00
|
|
|
|
let config = ToolsConfig::new(&ToolsConfigParams {
|
|
|
|
|
|
model_family: &model_family,
|
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
|
sandbox_policy: SandboxPolicy::ReadOnly,
|
|
|
|
|
|
include_plan_tool: false,
|
|
|
|
|
|
include_apply_patch_tool: false,
|
|
|
|
|
|
include_web_search_request: true,
|
|
|
|
|
|
use_streamable_shell_tool: false,
|
2025-08-27 17:41:23 -07:00
|
|
|
|
include_view_image_tool: true,
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
experimental_unified_exec_tool: true,
|
2025-08-24 22:43:42 -07:00
|
|
|
|
});
|
2025-08-11 03:57:39 +03:00
|
|
|
|
|
|
|
|
|
|
let tools = get_openai_tools(
|
|
|
|
|
|
&config,
|
|
|
|
|
|
Some(HashMap::from([(
|
|
|
|
|
|
"dash/value".to_string(),
|
|
|
|
|
|
mcp_types::Tool {
|
|
|
|
|
|
name: "value".to_string(),
|
|
|
|
|
|
input_schema: ToolInputSchema {
|
|
|
|
|
|
properties: Some(serde_json::json!({
|
|
|
|
|
|
"value": { "anyOf": [ { "type": "string" }, { "type": "number" } ] }
|
|
|
|
|
|
})),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
r#type: "object".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
output_schema: None,
|
|
|
|
|
|
title: None,
|
|
|
|
|
|
annotations: None,
|
|
|
|
|
|
description: Some("AnyOf Value".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
)])),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
Unified execution (#3288)
## Unified PTY-Based Exec Tool
Note: this requires to have this flag in the config:
`use_experimental_unified_exec_tool=true`
- Adds a PTY-backed interactive exec feature (“unified_exec”) with
session reuse via
session_id, bounded output (128 KiB), and timeout clamping (≤ 60 s).
- Protocol: introduces ResponseItem::UnifiedExec { session_id,
arguments, timeout_ms }.
- Tools: exposes unified_exec as a function tool (Responses API);
excluded from Chat
Completions payload while still supported in tool lists.
- Path handling: resolves commands via PATH (or explicit paths), with
UTF‑8/newline‑aware
truncation (truncate_middle).
- Tests: cover command parsing, path resolution, session
persistence/cleanup, multi‑session
isolation, timeouts, and truncation behavior.
2025-09-10 17:38:11 -07:00
|
|
|
|
assert_eq_tool_names(
|
|
|
|
|
|
&tools,
|
|
|
|
|
|
&["unified_exec", "web_search", "view_image", "dash/value"],
|
|
|
|
|
|
);
|
2025-08-11 03:57:39 +03:00
|
|
|
|
assert_eq!(
|
2025-08-27 17:41:23 -07:00
|
|
|
|
tools[3],
|
2025-08-11 03:57:39 +03:00
|
|
|
|
OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
name: "dash/value".to_string(),
|
|
|
|
|
|
parameters: JsonSchema::Object {
|
|
|
|
|
|
properties: BTreeMap::from([(
|
|
|
|
|
|
"value".to_string(),
|
|
|
|
|
|
JsonSchema::String { description: None }
|
|
|
|
|
|
)]),
|
|
|
|
|
|
required: None,
|
|
|
|
|
|
additional_properties: None,
|
|
|
|
|
|
},
|
|
|
|
|
|
description: "AnyOf Value".to_string(),
|
|
|
|
|
|
strict: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-03 10:02:34 -07:00
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_shell_tool_for_sandbox_workspace_write() {
|
|
|
|
|
|
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
|
|
|
|
|
writable_roots: vec!["workspace".into()],
|
|
|
|
|
|
network_access: false,
|
|
|
|
|
|
exclude_tmpdir_env_var: false,
|
|
|
|
|
|
exclude_slash_tmp: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
let tool = super::create_shell_tool_for_sandbox(&sandbox_policy);
|
|
|
|
|
|
let OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
description, name, ..
|
|
|
|
|
|
}) = &tool
|
|
|
|
|
|
else {
|
|
|
|
|
|
panic!("expected function tool");
|
|
|
|
|
|
};
|
|
|
|
|
|
assert_eq!(name, "shell");
|
|
|
|
|
|
|
2025-09-12 14:24:09 -04:00
|
|
|
|
let expected = "Runs a shell command and returns its output.";
|
2025-09-03 10:02:34 -07:00
|
|
|
|
assert_eq!(description, expected);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_shell_tool_for_sandbox_readonly() {
|
|
|
|
|
|
let tool = super::create_shell_tool_for_sandbox(&SandboxPolicy::ReadOnly);
|
|
|
|
|
|
let OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
description, name, ..
|
|
|
|
|
|
}) = &tool
|
|
|
|
|
|
else {
|
|
|
|
|
|
panic!("expected function tool");
|
|
|
|
|
|
};
|
|
|
|
|
|
assert_eq!(name, "shell");
|
|
|
|
|
|
|
2025-09-12 14:24:09 -04:00
|
|
|
|
let expected = "Runs a shell command and returns its output.";
|
2025-09-03 10:02:34 -07:00
|
|
|
|
assert_eq!(description, expected);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_shell_tool_for_sandbox_danger_full_access() {
|
|
|
|
|
|
let tool = super::create_shell_tool_for_sandbox(&SandboxPolicy::DangerFullAccess);
|
|
|
|
|
|
let OpenAiTool::Function(ResponsesApiTool {
|
|
|
|
|
|
description, name, ..
|
|
|
|
|
|
}) = &tool
|
|
|
|
|
|
else {
|
|
|
|
|
|
panic!("expected function tool");
|
|
|
|
|
|
};
|
|
|
|
|
|
assert_eq!(name, "shell");
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(description, "Runs a shell command and returns its output.");
|
|
|
|
|
|
}
|
2025-08-05 19:27:52 -07:00
|
|
|
|
}
|