mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:52:55 +08:00
Merge pull request #435 from MuRo-J/fix/tool-parameter-aliases
fix: accept common tool parameter aliases
This commit is contained in:
@@ -12,6 +12,18 @@ pub fn tool_query(id: u32, call: &ToolCall) -> Result<pb::AgentServerMessage> {
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| Error::Protocol(format!("{} is missing {name}", call.name)))
|
||||
};
|
||||
// Claude 系模型常按 Claude Code 习惯输出别名参数(如 query),逐个回退兼容。
|
||||
let string_aliased = |names: &[&str]| -> Result<String> {
|
||||
for name in names {
|
||||
if let Some(value) = call.arguments.get(name).and_then(Value::as_str) {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
}
|
||||
Err(Error::Protocol(format!(
|
||||
"{} is missing {}",
|
||||
call.name, names[0]
|
||||
)))
|
||||
};
|
||||
let optional_string = |name: &str| {
|
||||
call.arguments
|
||||
.get(name)
|
||||
@@ -80,7 +92,7 @@ pub fn tool_query(id: u32, call: &ToolCall) -> Result<pb::AgentServerMessage> {
|
||||
}
|
||||
"websearch" => Query::WebSearchRequestQuery(pb::WebSearchRequestQuery {
|
||||
args: Some(pb::WebSearchArgs {
|
||||
search_term: string("search_term")?,
|
||||
search_term: string_aliased(&["search_term", "query"])?,
|
||||
tool_call_id: call.call_id.clone(),
|
||||
}),
|
||||
}),
|
||||
@@ -214,3 +226,37 @@ fn normalized(value: &str) -> String {
|
||||
.flat_map(char::to_lowercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::tool_query;
|
||||
use crate::cursor::protocol::proto::agent::v1 as pb;
|
||||
use crate::model::ToolCall;
|
||||
|
||||
#[test]
|
||||
fn web_search_accepts_query_as_search_term_alias() {
|
||||
// Claude 系模型常按 Claude Code 习惯发送 query 而非 search_term,
|
||||
// 交互查询编码必须接受别名,而不是报 `WebSearch is missing search_term`。
|
||||
let call = ToolCall {
|
||||
index: 0,
|
||||
call_id: "call-1".into(),
|
||||
model_call_id: "model-1".into(),
|
||||
name: "WebSearch".into(),
|
||||
arguments_text: String::new(),
|
||||
arguments: json!({ "query": "lmarena leaderboard" }),
|
||||
argument_error: None,
|
||||
};
|
||||
let message = tool_query(1, &call).unwrap();
|
||||
let Some(pb::agent_server_message::Message::InteractionQuery(query)) = message.message
|
||||
else {
|
||||
panic!("expected an InteractionQuery");
|
||||
};
|
||||
let Some(pb::interaction_query::Query::WebSearchRequestQuery(request)) = query.query
|
||||
else {
|
||||
panic!("expected a WebSearchRequestQuery");
|
||||
};
|
||||
assert_eq!(request.args.unwrap().search_term, "lmarena leaderboard");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,20 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
};
|
||||
// 与执行侧一致的参数别名兼容(如 Claude Code 习惯的 file_path),仅影响展示。
|
||||
let aliased = |names: &[&str]| -> String {
|
||||
names
|
||||
.iter()
|
||||
.find_map(|name| call.arguments.get(name).and_then(Value::as_str))
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
};
|
||||
let optional_aliased = |names: &[&str]| -> Option<String> {
|
||||
names
|
||||
.iter()
|
||||
.find_map(|name| call.arguments.get(name).and_then(Value::as_str))
|
||||
.map(str::to_string)
|
||||
};
|
||||
match output.tool.as_mut() {
|
||||
Some(pb::tool_call::Tool::ShellToolCall(tool)) => {
|
||||
tool.description = optional("description");
|
||||
@@ -299,7 +313,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
||||
}
|
||||
Some(pb::tool_call::Tool::DeleteToolCall(tool)) => {
|
||||
tool.args = Some(pb::DeleteArgs {
|
||||
path: string("path"),
|
||||
path: aliased(&["path", "file_path", "filePath"]),
|
||||
tool_call_id: call.call_id.clone(),
|
||||
})
|
||||
}
|
||||
@@ -321,7 +335,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
||||
}
|
||||
Some(pb::tool_call::Tool::ReadToolCall(tool)) => {
|
||||
tool.args = Some(pb::ReadToolArgs {
|
||||
path: string("path"),
|
||||
path: aliased(&["path", "file_path", "filePath"]),
|
||||
offset: call
|
||||
.arguments
|
||||
.get("offset")
|
||||
@@ -350,7 +364,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
||||
}
|
||||
Some(pb::tool_call::Tool::EditToolCall(tool)) => {
|
||||
let stream_content = if normalized(&call.name) == "write" {
|
||||
optional("contents").unwrap_or_default()
|
||||
optional_aliased(&["contents", "content"]).unwrap_or_default()
|
||||
} else {
|
||||
optional("new_string").unwrap_or_default()
|
||||
};
|
||||
@@ -358,7 +372,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
||||
path: if normalized(&call.name) == "editnotebook" {
|
||||
string("target_notebook")
|
||||
} else {
|
||||
string("path")
|
||||
aliased(&["path", "file_path", "filePath"])
|
||||
},
|
||||
stream_content: Some(edit::normalize_newlines(&stream_content)),
|
||||
})
|
||||
@@ -418,7 +432,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
||||
}
|
||||
Some(pb::tool_call::Tool::WebSearchToolCall(tool)) => {
|
||||
tool.args = Some(pb::WebSearchArgs {
|
||||
search_term: string("search_term"),
|
||||
search_term: aliased(&["search_term", "query"]),
|
||||
tool_call_id: call.call_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,18 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| Error::Protocol(format!("{} is missing {name}", call.name)))
|
||||
};
|
||||
// Claude 系模型常按 Claude Code 习惯输出别名参数(如 file_path),逐个回退兼容。
|
||||
let string_aliased = |names: &[&str]| -> Result<String> {
|
||||
for name in names {
|
||||
if let Some(value) = call.arguments.get(name).and_then(Value::as_str) {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
}
|
||||
Err(Error::Protocol(format!(
|
||||
"{} is missing {}",
|
||||
call.name, names[0]
|
||||
)))
|
||||
};
|
||||
let optional_string = |name: &str| {
|
||||
call.arguments
|
||||
.get(name)
|
||||
@@ -63,7 +75,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
|
||||
})
|
||||
}
|
||||
"read" => Message::ReadArgs(pb::ReadArgs {
|
||||
path: string("path")?,
|
||||
path: string_aliased(&["path", "file_path", "filePath"])?,
|
||||
tool_call_id: call.call_id.clone(),
|
||||
offset: int("offset"),
|
||||
limit: call
|
||||
@@ -74,7 +86,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
|
||||
encoding_hint: optional_string("encoding_hint"),
|
||||
}),
|
||||
"delete" => Message::DeleteArgs(pb::DeleteArgs {
|
||||
path: string("path")?,
|
||||
path: string_aliased(&["path", "file_path", "filePath"])?,
|
||||
tool_call_id: call.call_id.clone(),
|
||||
}),
|
||||
"grep" => Message::GrepArgs(pb::GrepArgs {
|
||||
|
||||
@@ -13,12 +13,12 @@ pub(crate) struct EditWrite {
|
||||
}
|
||||
|
||||
pub(crate) fn path(call: &ToolCall) -> Result<String> {
|
||||
let field = if normalized(&call.name) == "editnotebook" {
|
||||
"target_notebook"
|
||||
if normalized(&call.name) == "editnotebook" {
|
||||
string(call, "target_notebook")
|
||||
} else {
|
||||
"path"
|
||||
};
|
||||
string(call, field)
|
||||
// Claude 系模型常按 Claude Code 习惯输出 file_path/filePath,做别名兼容。
|
||||
string_any(call, &["path", "file_path", "filePath"])
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn execution_path(call: &ToolCall) -> Result<Option<String>> {
|
||||
@@ -63,7 +63,10 @@ pub(crate) fn after_read(
|
||||
};
|
||||
let after = match normalized(&call.name).as_str() {
|
||||
"write" => {
|
||||
normalize_newlines(&string(call, "contents").map_err(|error| error.to_string())?)
|
||||
// Claude Code 习惯的 content 作为 contents 的别名兼容。
|
||||
normalize_newlines(
|
||||
&string_any(call, &["contents", "content"]).map_err(|error| error.to_string())?,
|
||||
)
|
||||
}
|
||||
"strreplace" => replace_string(call, &before)?,
|
||||
"editnotebook" => edit_notebook(call, &before)?,
|
||||
@@ -230,11 +233,19 @@ fn source_lines(value: &str) -> Vec<Value> {
|
||||
}
|
||||
|
||||
fn string(call: &ToolCall, field: &str) -> Result<String> {
|
||||
call.arguments
|
||||
.get(field)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
.ok_or_else(|| Error::Protocol(format!("{} is missing {field}", call.name)))
|
||||
string_any(call, &[field])
|
||||
}
|
||||
|
||||
fn string_any(call: &ToolCall, fields: &[&str]) -> Result<String> {
|
||||
for field in fields {
|
||||
if let Some(value) = call.arguments.get(field).and_then(Value::as_str) {
|
||||
return Ok(value.to_owned());
|
||||
}
|
||||
}
|
||||
Err(Error::Protocol(format!(
|
||||
"{} is missing {}",
|
||||
call.name, fields[0]
|
||||
)))
|
||||
}
|
||||
|
||||
fn normalized(value: &str) -> String {
|
||||
|
||||
@@ -88,12 +88,17 @@ fn start_web_search(
|
||||
search: WebSearch,
|
||||
pending: PendingInteraction,
|
||||
) -> Result<()> {
|
||||
let query = pending
|
||||
.call
|
||||
.arguments
|
||||
.get("search_term")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|query| !query.trim().is_empty())
|
||||
// Claude Code 习惯的 query 作为 search_term 的别名兼容。
|
||||
let query = ["search_term", "query"]
|
||||
.iter()
|
||||
.find_map(|name| {
|
||||
pending
|
||||
.call
|
||||
.arguments
|
||||
.get(name)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
})
|
||||
.ok_or_else(|| Error::Protocol("WebSearch is missing search_term".into()))?
|
||||
.to_string();
|
||||
tokio::spawn(async move {
|
||||
|
||||
Reference in New Issue
Block a user