mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-08 15:43:10 +08:00
fix(tools): complete the bash shell alias in the tool codec
The tool dispatcher already treats `bash`/`Bash` as an alias of `Shell` (routing, `is_shell_tool`, and `block_until_ms` normalization), but the codec only matched `shell`: - `tool_placeholder` returned `unsupported tool: bash`, which aborts the turn while streaming the tool call, before it ever runs; - `request` returned `tool bash is not executed through ExecServerMessage` (after already reserving an exec slot); and - `stream_closed` built its shell-specific error result only for `Shell`. Anthropic models frequently emit `Bash` even when the tool is advertised as `Shell`, so the alias must hold across the codec. Match `bash` wherever the codec special-cases `shell`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
76003a907a
commit
9e2d22418b
@@ -167,7 +167,7 @@ pub fn tool_completed(call: &ToolCall, completion: &ToolCompletion) -> pb::Agent
|
|||||||
pub fn tool_placeholder(name: &str, call_id: &str) -> Result<pb::ToolCall> {
|
pub fn tool_placeholder(name: &str, call_id: &str) -> Result<pb::ToolCall> {
|
||||||
use pb::tool_call::Tool;
|
use pb::tool_call::Tool;
|
||||||
let tool = match normalized(name).as_str() {
|
let tool = match normalized(name).as_str() {
|
||||||
"shell" => Tool::ShellToolCall(pb::ShellToolCall::default()),
|
"shell" | "bash" => Tool::ShellToolCall(pb::ShellToolCall::default()),
|
||||||
"delete" => Tool::DeleteToolCall(pb::DeleteToolCall::default()),
|
"delete" => Tool::DeleteToolCall(pb::DeleteToolCall::default()),
|
||||||
"glob" => Tool::GlobToolCall(pb::GlobToolCall::default()),
|
"glob" => Tool::GlobToolCall(pb::GlobToolCall::default()),
|
||||||
"grep" => Tool::GrepToolCall(pb::GrepToolCall::default()),
|
"grep" => Tool::GrepToolCall(pb::GrepToolCall::default()),
|
||||||
@@ -527,3 +527,23 @@ fn now_ms() -> u64 {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_millis() as u64
|
.as_millis() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::tool_placeholder;
|
||||||
|
use crate::cursor::protocol::proto::agent::v1 as pb;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bash_renders_as_a_shell_placeholder() {
|
||||||
|
// The dispatcher treats `bash`/`Bash` as a Shell alias, so the streaming
|
||||||
|
// placeholder must too; otherwise a `Bash` tool call aborts the turn with
|
||||||
|
// `unsupported tool: bash` before it ever runs.
|
||||||
|
for name in ["shell", "Shell", "bash", "Bash"] {
|
||||||
|
let tool = tool_placeholder(name, "call-1").unwrap().tool;
|
||||||
|
assert!(
|
||||||
|
matches!(tool, Some(pb::tool_call::Tool::ShellToolCall(_))),
|
||||||
|
"{name} should render as a Shell tool"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
|
|||||||
.map(|v| v as i32)
|
.map(|v| v as i32)
|
||||||
};
|
};
|
||||||
let message = match normalize(&call.name).as_str() {
|
let message = match normalize(&call.name).as_str() {
|
||||||
"shell" => {
|
"shell" | "bash" => {
|
||||||
let command = string("command")?;
|
let command = string("command")?;
|
||||||
let (simple_commands, parsing_result) = shell_command_metadata(&command);
|
let (simple_commands, parsing_result) = shell_command_metadata(&command);
|
||||||
Message::ShellStreamArgs(pb::ShellArgs {
|
Message::ShellStreamArgs(pb::ShellArgs {
|
||||||
@@ -520,3 +520,37 @@ fn prost_value(value: &Value) -> prost_types::Value {
|
|||||||
};
|
};
|
||||||
ProstValue { kind: Some(kind) }
|
ProstValue { kind: Some(kind) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::request;
|
||||||
|
use crate::cursor::protocol::proto::agent::v1 as pb;
|
||||||
|
use crate::cursor::tools::runtime::ExecContext;
|
||||||
|
use crate::model::ToolCall;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bash_is_encoded_as_a_shell_exec_request() {
|
||||||
|
// The dispatcher routes `bash`/`Bash` to the shell executor, so the
|
||||||
|
// request codec must encode it as a Shell stream instead of erroring
|
||||||
|
// with `tool bash is not executed through ExecServerMessage`.
|
||||||
|
let call = ToolCall {
|
||||||
|
index: 0,
|
||||||
|
call_id: "call-1".into(),
|
||||||
|
model_call_id: "model-1".into(),
|
||||||
|
name: "Bash".into(),
|
||||||
|
arguments_text: String::new(),
|
||||||
|
arguments: json!({ "command": "ls -la" }),
|
||||||
|
};
|
||||||
|
let message = request(1, &call, &ExecContext::default()).unwrap();
|
||||||
|
let Some(pb::agent_server_message::Message::ExecServerMessage(exec)) = message.message
|
||||||
|
else {
|
||||||
|
panic!("expected an ExecServerMessage");
|
||||||
|
};
|
||||||
|
let Some(pb::exec_server_message::Message::ShellStreamArgs(args)) = exec.message else {
|
||||||
|
panic!("expected ShellStreamArgs");
|
||||||
|
};
|
||||||
|
assert_eq!(args.command, "ls -la");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ pub async fn stream_closed(id: u32, pending: &CursorToolRuntime) -> Result<Optio
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let error = "Cursor Exec stream closed before returning a terminal result";
|
let error = "Cursor Exec stream closed before returning a terminal result";
|
||||||
if entry.call.name.eq_ignore_ascii_case("Shell") {
|
if entry.call.name.eq_ignore_ascii_case("Shell") || entry.call.name.eq_ignore_ascii_case("Bash")
|
||||||
|
{
|
||||||
let command = entry
|
let command = entry
|
||||||
.call
|
.call
|
||||||
.arguments
|
.arguments
|
||||||
|
|||||||
Reference in New Issue
Block a user