mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Extract tool spec helpers into codex-tools (#16471)
## Why Follow-up to #16379. `codex-rs/core/src/tools/spec.rs` and the corresponding handlers still owned several pure tool-definition helpers even though they do not need `codex-core` runtime state. Keeping that spec-only logic in `codex-core` keeps the crate boundary blurry and works against the guidance in `AGENTS.md` to keep shared tooling out of `codex-core` when possible. This change takes another step toward a dedicated `codex-tools` crate by moving more metadata and schema-building code behind the `codex-tools` API while leaving the actual tool execution paths in `codex-core`. ## What Changed - Added `codex-rs/tools/src/apply_patch_tool.rs` to own `ApplyPatchToolArgs`, the freeform/json `apply_patch` tool specs, and the moved `tool_apply_patch.lark` grammar. - Updated `codex-rs/tools/BUILD.bazel` so Bazel exposes the moved grammar file to `codex-tools`. - Moved the `request_user_input` availability and description helpers into `codex-rs/tools/src/request_user_input_tool.rs`, with the related unit tests moved alongside that business logic. - Moved `request_permissions_tool_description()` into `codex-rs/tools/src/local_tool.rs`. - Rewired `codex-rs/core/src/tools/spec.rs`, `codex-rs/core/src/tools/handlers/apply_patch.rs`, and `codex-rs/core/src/tools/handlers/request_user_input.rs` to consume the new `codex-tools` exports instead of local helper code. - Removed the now-redundant helper implementations and tests from `codex-core`, plus a couple of stale `client_common` re-exports that became unused after the move. ## Testing - `cargo test -p codex-tools` - `cargo test -p codex-core tools::spec::tests` - `cargo test -p codex-core tools::handlers::apply_patch::tests`
This commit is contained in:
committed by
GitHub
Unverified
parent
323aa968c3
commit
e6f5451a2c
@@ -157,8 +157,6 @@ fn strip_total_output_header(output: &str) -> Option<(&str, u32)> {
|
||||
}
|
||||
|
||||
pub(crate) mod tools {
|
||||
pub(crate) use codex_tools::FreeformTool;
|
||||
pub(crate) use codex_tools::FreeformToolFormat;
|
||||
pub(crate) use codex_tools::ResponsesApiTool;
|
||||
pub(crate) use codex_tools::ToolSearchOutputTool;
|
||||
pub(crate) use codex_tools::ToolSpec;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::apply_patch;
|
||||
use crate::apply_patch::InternalApplyPatchInvocation;
|
||||
use crate::apply_patch::convert_apply_patch_to_protocol;
|
||||
use crate::client_common::tools::FreeformTool;
|
||||
use crate::client_common::tools::FreeformToolFormat;
|
||||
use crate::client_common::tools::ResponsesApiTool;
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
@@ -26,8 +21,6 @@ use crate::tools::registry::ToolKind;
|
||||
use crate::tools::runtimes::apply_patch::ApplyPatchRequest;
|
||||
use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::spec::ApplyPatchToolArgs;
|
||||
use crate::tools::spec::JsonSchema;
|
||||
use async_trait::async_trait;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::ApplyPatchFileChange;
|
||||
@@ -36,14 +29,13 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
|
||||
use codex_tools::ApplyPatchToolArgs;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ApplyPatchHandler;
|
||||
|
||||
const APPLY_PATCH_LARK_GRAMMAR: &str = include_str!("tool_apply_patch.lark");
|
||||
|
||||
fn file_paths_for_action(action: &ApplyPatchAction) -> Vec<AbsolutePathBuf> {
|
||||
let mut keys = Vec::new();
|
||||
let cwd = action.cwd.as_path();
|
||||
@@ -356,112 +348,6 @@ pub(crate) async fn intercept_apply_patch(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a custom tool that can be used to edit files. Well-suited for GPT-5 models
|
||||
/// https://platform.openai.com/docs/guides/function-calling#custom-tools
|
||||
pub(crate) fn create_apply_patch_freeform_tool() -> ToolSpec {
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "apply_patch".to_string(),
|
||||
description: "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.".to_string(),
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
syntax: "lark".to_string(),
|
||||
definition: APPLY_PATCH_LARK_GRAMMAR.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a json tool that can be used to edit files. Should only be used with gpt-oss models
|
||||
pub(crate) fn create_apply_patch_json_tool() -> ToolSpec {
|
||||
let mut properties = BTreeMap::new();
|
||||
properties.insert(
|
||||
"input".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(r#"The entire contents of the apply_patch command"#.to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "apply_patch".to_string(),
|
||||
description: r#"Use the `apply_patch` tool to edit files.
|
||||
Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
|
||||
|
||||
*** Begin Patch
|
||||
[ one or more file sections ]
|
||||
*** End Patch
|
||||
|
||||
Within that envelope, you get a sequence of file operations.
|
||||
You MUST include a header to specify the action you are taking.
|
||||
Each operation starts with one of three headers:
|
||||
|
||||
*** Add File: <path> - create a new file. Every following line is a + line (the initial contents).
|
||||
*** Delete File: <path> - remove an existing file. Nothing follows.
|
||||
*** Update File: <path> - patch an existing file in place (optionally with a rename).
|
||||
|
||||
May be immediately followed by *** Move to: <new path> if you want to rename the file.
|
||||
Then one or more “hunks”, each introduced by @@ (optionally followed by a hunk header).
|
||||
Within a hunk each line starts with:
|
||||
|
||||
For instructions on [context_before] and [context_after]:
|
||||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
|
||||
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
||||
@@ class BaseClass
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
|
||||
- If a code block is repeated so many times in a class or function such that even a single `@@` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
|
||||
|
||||
@@ class BaseClass
|
||||
@@ def method():
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
|
||||
The full grammar definition is below:
|
||||
Patch := Begin { FileOp } End
|
||||
Begin := "*** Begin Patch" NEWLINE
|
||||
End := "*** End Patch" NEWLINE
|
||||
FileOp := AddFile | DeleteFile | UpdateFile
|
||||
AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE }
|
||||
DeleteFile := "*** Delete File: " path NEWLINE
|
||||
UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk }
|
||||
MoveTo := "*** Move to: " newPath NEWLINE
|
||||
Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ]
|
||||
HunkLine := (" " | "-" | "+") text NEWLINE
|
||||
|
||||
A full patch can combine several operations:
|
||||
|
||||
*** Begin Patch
|
||||
*** Add File: hello.txt
|
||||
+Hello world
|
||||
*** Update File: src/app.py
|
||||
*** Move to: src/main.py
|
||||
@@ def greet():
|
||||
-print("Hi")
|
||||
+print("Hello, world!")
|
||||
*** Delete File: obsolete.txt
|
||||
*** End Patch
|
||||
|
||||
It is important to remember:
|
||||
|
||||
- You must include a header with your intended action (Add/Delete/Update)
|
||||
- You must prefix new lines with `+` even when creating a new file
|
||||
- File references can only be relative, NEVER ABSOLUTE.
|
||||
"#
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["input".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "apply_patch_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -44,9 +44,7 @@ pub use mcp::McpHandler;
|
||||
pub use mcp_resource::McpResourceHandler;
|
||||
pub use plan::PlanHandler;
|
||||
pub use request_permissions::RequestPermissionsHandler;
|
||||
pub(crate) use request_permissions::request_permissions_tool_description;
|
||||
pub use request_user_input::RequestUserInputHandler;
|
||||
pub(crate) use request_user_input::request_user_input_tool_description;
|
||||
pub use shell::ShellCommandHandler;
|
||||
pub use shell::ShellHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
|
||||
@@ -10,11 +10,6 @@ use crate::tools::handlers::parse_arguments_with_base_path;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
|
||||
pub(crate) fn request_permissions_tool_description() -> String {
|
||||
"Request additional filesystem or network permissions from the user and wait for the client to grant a subset of the requested permission profile. Granted permissions apply automatically to later shell-like commands in the current turn, or for the rest of the session if the client approves them at session scope."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub struct RequestPermissionsHandler;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -6,50 +6,8 @@ use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
|
||||
fn request_user_input_is_available(mode: ModeKind, default_mode_request_user_input: bool) -> bool {
|
||||
mode.allows_request_user_input()
|
||||
|| (default_mode_request_user_input && mode == ModeKind::Default)
|
||||
}
|
||||
|
||||
fn format_allowed_modes(default_mode_request_user_input: bool) -> String {
|
||||
let mode_names: Vec<&str> = TUI_VISIBLE_COLLABORATION_MODES
|
||||
.into_iter()
|
||||
.filter(|mode| request_user_input_is_available(*mode, default_mode_request_user_input))
|
||||
.map(ModeKind::display_name)
|
||||
.collect();
|
||||
|
||||
match mode_names.as_slice() {
|
||||
[] => "no modes".to_string(),
|
||||
[mode] => format!("{mode} mode"),
|
||||
[first, second] => format!("{first} or {second} mode"),
|
||||
[..] => format!("modes: {}", mode_names.join(",")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn request_user_input_unavailable_message(
|
||||
mode: ModeKind,
|
||||
default_mode_request_user_input: bool,
|
||||
) -> Option<String> {
|
||||
if request_user_input_is_available(mode, default_mode_request_user_input) {
|
||||
None
|
||||
} else {
|
||||
let mode_name = mode.display_name();
|
||||
Some(format!(
|
||||
"request_user_input is unavailable in {mode_name} mode"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn request_user_input_tool_description(default_mode_request_user_input: bool) -> String {
|
||||
let allowed_modes = format_allowed_modes(default_mode_request_user_input);
|
||||
format!(
|
||||
"Request user input for one to three short questions and wait for the response. This tool is only available in {allowed_modes}."
|
||||
)
|
||||
}
|
||||
use codex_tools::request_user_input_unavailable_message;
|
||||
|
||||
pub struct RequestUserInputHandler {
|
||||
pub default_mode_request_user_input: bool,
|
||||
@@ -119,7 +77,3 @@ impl ToolHandler for RequestUserInputHandler {
|
||||
Ok(FunctionToolOutput::from_text(content, Some(true)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "request_user_input_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn request_user_input_mode_availability_defaults_to_plan_only() {
|
||||
assert!(ModeKind::Plan.allows_request_user_input());
|
||||
assert!(!ModeKind::Default.allows_request_user_input());
|
||||
assert!(!ModeKind::Execute.allows_request_user_input());
|
||||
assert!(!ModeKind::PairProgramming.allows_request_user_input());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() {
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Plan,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Default,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
Some("request_user_input is unavailable in Default mode".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Default,
|
||||
/*default_mode_request_user_input*/ true
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Execute,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
Some("request_user_input is unavailable in Execute mode".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::PairProgramming,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
Some("request_user_input is unavailable in Pair Programming mode".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_user_input_tool_description_mentions_available_modes() {
|
||||
assert_eq!(
|
||||
request_user_input_tool_description(/*default_mode_request_user_input*/ false),
|
||||
"Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_tool_description(/*default_mode_request_user_input*/ true),
|
||||
"Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string()
|
||||
);
|
||||
}
|
||||
@@ -10,13 +10,9 @@ use crate::tools::handlers::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use crate::tools::handlers::TOOL_SEARCH_TOOL_NAME;
|
||||
use crate::tools::handlers::TOOL_SUGGEST_TOOL_NAME;
|
||||
use crate::tools::handlers::agent_jobs::BatchJobHandler;
|
||||
use crate::tools::handlers::apply_patch::create_apply_patch_freeform_tool;
|
||||
use crate::tools::handlers::apply_patch::create_apply_patch_json_tool;
|
||||
use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::request_permissions_tool_description;
|
||||
use crate::tools::handlers::request_user_input_tool_description;
|
||||
use crate::tools::registry::ToolRegistryBuilder;
|
||||
use crate::tools::registry::tool_handler_key;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
@@ -35,6 +31,8 @@ use codex_tools::ToolUserShellType;
|
||||
use codex_tools::ViewImageToolOptions;
|
||||
use codex_tools::WaitAgentTimeoutOptions;
|
||||
use codex_tools::augment_tool_spec_for_code_mode;
|
||||
use codex_tools::create_apply_patch_freeform_tool;
|
||||
use codex_tools::create_apply_patch_json_tool;
|
||||
use codex_tools::create_assign_task_tool;
|
||||
use codex_tools::create_close_agent_tool_v1;
|
||||
use codex_tools::create_close_agent_tool_v2;
|
||||
@@ -68,9 +66,9 @@ use codex_tools::create_wait_tool;
|
||||
use codex_tools::create_write_stdin_tool;
|
||||
use codex_tools::dynamic_tool_to_responses_api_tool;
|
||||
use codex_tools::mcp_tool_to_responses_api_tool;
|
||||
use codex_tools::request_permissions_tool_description;
|
||||
use codex_tools::request_user_input_tool_description;
|
||||
use codex_tools::tool_spec_to_code_mode_tool_definition;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type JsonSchema = codex_tools::JsonSchema;
|
||||
@@ -103,12 +101,6 @@ fn agent_type_description(config: &ToolsConfig) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO(dylan): deprecate once we get rid of json tool
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct ApplyPatchToolArgs {
|
||||
pub(crate) input: String,
|
||||
}
|
||||
|
||||
fn push_tool_spec(
|
||||
builder: &mut ToolRegistryBuilder,
|
||||
spec: ToolSpec,
|
||||
|
||||
@@ -30,6 +30,7 @@ use codex_tools::ResponsesApiWebSearchUserLocation;
|
||||
use codex_tools::SpawnAgentToolOptions;
|
||||
use codex_tools::ViewImageToolOptions;
|
||||
use codex_tools::WaitAgentTimeoutOptions;
|
||||
use codex_tools::create_apply_patch_freeform_tool;
|
||||
use codex_tools::create_close_agent_tool_v1;
|
||||
use codex_tools::create_close_agent_tool_v2;
|
||||
use codex_tools::create_exec_command_tool;
|
||||
@@ -45,6 +46,8 @@ use codex_tools::create_wait_agent_tool_v1;
|
||||
use codex_tools::create_wait_agent_tool_v2;
|
||||
use codex_tools::create_write_stdin_tool;
|
||||
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
|
||||
use codex_tools::request_permissions_tool_description;
|
||||
use codex_tools::request_user_input_tool_description;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -3,4 +3,7 @@ load("//:defs.bzl", "codex_rust_crate")
|
||||
codex_rust_crate(
|
||||
name = "tools",
|
||||
crate_name = "codex_tools",
|
||||
compile_data = [
|
||||
"src/tool_apply_patch.lark",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
use crate::FreeformTool;
|
||||
use crate::FreeformToolFormat;
|
||||
use crate::JsonSchema;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolSpec;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const APPLY_PATCH_LARK_GRAMMAR: &str = include_str!("tool_apply_patch.lark");
|
||||
|
||||
const APPLY_PATCH_JSON_TOOL_DESCRIPTION: &str = r#"Use the `apply_patch` tool to edit files.
|
||||
Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
|
||||
|
||||
*** Begin Patch
|
||||
[ one or more file sections ]
|
||||
*** End Patch
|
||||
|
||||
Within that envelope, you get a sequence of file operations.
|
||||
You MUST include a header to specify the action you are taking.
|
||||
Each operation starts with one of three headers:
|
||||
|
||||
*** Add File: <path> - create a new file. Every following line is a + line (the initial contents).
|
||||
*** Delete File: <path> - remove an existing file. Nothing follows.
|
||||
*** Update File: <path> - patch an existing file in place (optionally with a rename).
|
||||
|
||||
May be immediately followed by *** Move to: <new path> if you want to rename the file.
|
||||
Then one or more “hunks”, each introduced by @@ (optionally followed by a hunk header).
|
||||
Within a hunk each line starts with:
|
||||
|
||||
For instructions on [context_before] and [context_after]:
|
||||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
|
||||
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
||||
@@ class BaseClass
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
|
||||
- If a code block is repeated so many times in a class or function such that even a single `@@` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
|
||||
|
||||
@@ class BaseClass
|
||||
@@ def method():
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
|
||||
The full grammar definition is below:
|
||||
Patch := Begin { FileOp } End
|
||||
Begin := "*** Begin Patch" NEWLINE
|
||||
End := "*** End Patch" NEWLINE
|
||||
FileOp := AddFile | DeleteFile | UpdateFile
|
||||
AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE }
|
||||
DeleteFile := "*** Delete File: " path NEWLINE
|
||||
UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk }
|
||||
MoveTo := "*** Move to: " newPath NEWLINE
|
||||
Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ]
|
||||
HunkLine := (" " | "-" | "+") text NEWLINE
|
||||
|
||||
A full patch can combine several operations:
|
||||
|
||||
*** Begin Patch
|
||||
*** Add File: hello.txt
|
||||
+Hello world
|
||||
*** Update File: src/app.py
|
||||
*** Move to: src/main.py
|
||||
@@ def greet():
|
||||
-print("Hi")
|
||||
+print("Hello, world!")
|
||||
*** Delete File: obsolete.txt
|
||||
*** End Patch
|
||||
|
||||
It is important to remember:
|
||||
|
||||
- You must include a header with your intended action (Add/Delete/Update)
|
||||
- You must prefix new lines with `+` even when creating a new file
|
||||
- File references can only be relative, NEVER ABSOLUTE.
|
||||
"#;
|
||||
|
||||
/// TODO(dylan): deprecate once we get rid of json tool
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ApplyPatchToolArgs {
|
||||
pub input: String,
|
||||
}
|
||||
|
||||
/// Returns a custom tool that can be used to edit files. Well-suited for GPT-5 models
|
||||
/// https://platform.openai.com/docs/guides/function-calling#custom-tools
|
||||
pub fn create_apply_patch_freeform_tool() -> ToolSpec {
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "apply_patch".to_string(),
|
||||
description: "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.".to_string(),
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
syntax: "lark".to_string(),
|
||||
definition: APPLY_PATCH_LARK_GRAMMAR.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a json tool that can be used to edit files. Should only be used with gpt-oss models
|
||||
pub fn create_apply_patch_json_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([(
|
||||
"input".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("The entire contents of the apply_patch command".to_string()),
|
||||
},
|
||||
)]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "apply_patch".to_string(),
|
||||
description: APPLY_PATCH_JSON_TOOL_DESCRIPTION.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["input".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "apply_patch_tool_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,47 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn create_apply_patch_freeform_tool_matches_expected_spec() {
|
||||
assert_eq!(
|
||||
create_apply_patch_freeform_tool(),
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "apply_patch".to_string(),
|
||||
description:
|
||||
"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON."
|
||||
.to_string(),
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
syntax: "lark".to_string(),
|
||||
definition: APPLY_PATCH_LARK_GRAMMAR.to_string(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_apply_patch_json_tool_matches_expected_spec() {
|
||||
assert_eq!(
|
||||
create_apply_patch_json_tool(),
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "apply_patch".to_string(),
|
||||
description: APPLY_PATCH_JSON_TOOL_DESCRIPTION.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties: BTreeMap::from([(
|
||||
"input".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"The entire contents of the apply_patch command".to_string(),
|
||||
),
|
||||
},
|
||||
)]),
|
||||
required: Some(vec!["input".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
mod agent_job_tool;
|
||||
mod agent_tool;
|
||||
mod apply_patch_tool;
|
||||
mod code_mode;
|
||||
mod dynamic_tool;
|
||||
mod image_detail;
|
||||
@@ -35,6 +36,9 @@ pub use agent_tool::create_spawn_agent_tool_v1;
|
||||
pub use agent_tool::create_spawn_agent_tool_v2;
|
||||
pub use agent_tool::create_wait_agent_tool_v1;
|
||||
pub use agent_tool::create_wait_agent_tool_v2;
|
||||
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::create_code_mode_tool;
|
||||
pub use code_mode::create_wait_tool;
|
||||
@@ -54,12 +58,15 @@ pub use local_tool::create_request_permissions_tool;
|
||||
pub use local_tool::create_shell_command_tool;
|
||||
pub use local_tool::create_shell_tool;
|
||||
pub use local_tool::create_write_stdin_tool;
|
||||
pub use local_tool::request_permissions_tool_description;
|
||||
pub use mcp_resource_tool::create_list_mcp_resource_templates_tool;
|
||||
pub use mcp_resource_tool::create_list_mcp_resources_tool;
|
||||
pub use mcp_resource_tool::create_read_mcp_resource_tool;
|
||||
pub use mcp_tool::mcp_call_tool_result_output_schema;
|
||||
pub use mcp_tool::parse_mcp_tool;
|
||||
pub use request_user_input_tool::create_request_user_input_tool;
|
||||
pub use request_user_input_tool::request_user_input_tool_description;
|
||||
pub use request_user_input_tool::request_user_input_unavailable_message;
|
||||
pub use responses_api::FreeformTool;
|
||||
pub use responses_api::FreeformToolFormat;
|
||||
pub use responses_api::ResponsesApiNamespace;
|
||||
|
||||
@@ -318,6 +318,11 @@ pub fn create_request_permissions_tool(description: String) -> ToolSpec {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn request_permissions_tool_description() -> String {
|
||||
"Request additional filesystem or network permissions from the user and wait for the client to grant a subset of the requested permission profile. Granted permissions apply automatically to later shell-like commands in the current turn, or for the rest of the session if the client approves them at session scope."
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn unified_exec_output_schema() -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::JsonSchema;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolSpec;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub fn create_request_user_input_tool(description: String) -> ToolSpec {
|
||||
@@ -89,6 +91,47 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn request_user_input_unavailable_message(
|
||||
mode: ModeKind,
|
||||
default_mode_request_user_input: bool,
|
||||
) -> Option<String> {
|
||||
if request_user_input_is_available(mode, default_mode_request_user_input) {
|
||||
None
|
||||
} else {
|
||||
let mode_name = mode.display_name();
|
||||
Some(format!(
|
||||
"request_user_input is unavailable in {mode_name} mode"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_user_input_tool_description(default_mode_request_user_input: bool) -> String {
|
||||
let allowed_modes = format_allowed_modes(default_mode_request_user_input);
|
||||
format!(
|
||||
"Request user input for one to three short questions and wait for the response. This tool is only available in {allowed_modes}."
|
||||
)
|
||||
}
|
||||
|
||||
fn request_user_input_is_available(mode: ModeKind, default_mode_request_user_input: bool) -> bool {
|
||||
mode.allows_request_user_input()
|
||||
|| (default_mode_request_user_input && mode == ModeKind::Default)
|
||||
}
|
||||
|
||||
fn format_allowed_modes(default_mode_request_user_input: bool) -> String {
|
||||
let mode_names: Vec<&str> = TUI_VISIBLE_COLLABORATION_MODES
|
||||
.into_iter()
|
||||
.filter(|mode| request_user_input_is_available(*mode, default_mode_request_user_input))
|
||||
.map(ModeKind::display_name)
|
||||
.collect();
|
||||
|
||||
match mode_names.as_slice() {
|
||||
[] => "no modes".to_string(),
|
||||
[mode] => format!("{mode} mode"),
|
||||
[first, second] => format!("{first} or {second} mode"),
|
||||
[..] => format!("modes: {}", mode_names.join(",")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "request_user_input_tool_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -100,3 +101,54 @@ fn request_user_input_tool_includes_questions_schema() {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() {
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Plan,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Default,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
Some("request_user_input is unavailable in Default mode".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Default,
|
||||
/*default_mode_request_user_input*/ true
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::Execute,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
Some("request_user_input is unavailable in Execute mode".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_unavailable_message(
|
||||
ModeKind::PairProgramming,
|
||||
/*default_mode_request_user_input*/ false
|
||||
),
|
||||
Some("request_user_input is unavailable in Pair Programming mode".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_user_input_tool_description_mentions_available_modes() {
|
||||
assert_eq!(
|
||||
request_user_input_tool_description(/*default_mode_request_user_input*/ false),
|
||||
"Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
request_user_input_tool_description(/*default_mode_request_user_input*/ true),
|
||||
"Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user