Files
cursor-byok/server/src/cursor/tools/result/exec/output.rs
T
2026-08-24 19:13:50 +08:00

515 lines
19 KiB
Rust

use crate::{cursor::proto::agent::v1 as pb, model::ToolCall, Error, Result};
pub(super) fn output(
message: &pb::exec_client_message::Message,
call: &ToolCall,
) -> Result<(String, bool)> {
use pb::exec_client_message::Message;
match message {
Message::ShellResult(value) | Message::MiniSweAgentBashResult(value) => shell(value),
Message::ReadResult(value) | Message::RedactedReadResult(value) => read(value),
Message::WriteResult(value) => write(value),
Message::DeleteResult(value) => delete(value),
Message::GrepResult(value) => grep(value),
Message::DiagnosticsResult(value) => diagnostics(value),
Message::McpResult(value) => mcp(value),
Message::ReadMcpResourceExecResult(value) => read_mcp(value),
Message::SubagentResult(value) => task(value, call),
_ => Err(Error::Protocol(
"unsupported terminal ExecClientMessage".into(),
)),
}
}
fn shell(value: &pb::ShellResult) -> Result<(String, bool)> {
use pb::shell_result::Result as R;
let output = match value.result.as_ref().ok_or_else(|| missing("shell"))? {
R::Success(success) if value.is_background == Some(true) => {
let mut fields = vec![format!("shell_id={}", success.shell_id.unwrap_or_default())];
if let Some(pid) = success.pid.or(value.pid) {
fields.push(format!("pid={pid}"));
}
if let Some(folder) = value.terminals_folder.as_deref().filter(|v| !v.is_empty()) {
fields.push(format!("terminals_folder={folder}"));
}
let output = streams(&success.stdout, &success.stderr);
let prefix = format!("shell running in background {}", fields.join(" "));
return Ok((
if output == "shell completed without output" {
prefix
} else {
format!("{prefix}\n{output}")
},
false,
));
}
R::Success(success) => return Ok((streams(&success.stdout, &success.stderr), false)),
R::Failure(failure) => streams(&failure.stdout, &failure.stderr),
R::Timeout(timeout) => format!(
"shell timed out after {}ms in {}",
timeout.timeout_ms, timeout.working_directory
),
R::Rejected(rejected) => rejected.reason.clone(),
R::SpawnError(error) => error.error.clone(),
R::PermissionDenied(denied) => denied.error.clone(),
};
Ok((output, true))
}
fn streams(stdout: &str, stderr: &str) -> String {
match (stdout.is_empty(), stderr.is_empty()) {
(false, false) => format!("{stdout}\n\n<stderr>\n{stderr}\n</stderr>"),
(false, true) => stdout.into(),
(true, false) => stderr.into(),
(true, true) => "shell completed without output".into(),
}
}
fn read(value: &pb::ReadResult) -> Result<(String, bool)> {
use pb::{read_result::Result as R, read_success::Output};
match value.result.as_ref().ok_or_else(|| missing("read"))? {
R::Success(success) => Ok((
match success.output.as_ref() {
Some(Output::Content(text)) => text.clone(),
Some(Output::Data(bytes)) => format!("read binary bytes={}", bytes.len()),
None => format!("read success path={}", success.path),
},
false,
)),
R::Error(error) => Ok((error.error.clone(), true)),
R::Rejected(rejected) => Ok((rejected.reason.clone(), true)),
R::FileNotFound(value) => Ok((format!("file not found: {}", value.path), true)),
R::PermissionDenied(value) => Ok((format!("permission denied: {}", value.path), true)),
R::InvalidFile(value) => Ok((value.reason.clone(), true)),
}
}
fn write(value: &pb::WriteResult) -> Result<(String, bool)> {
use pb::write_result::Result as R;
match value.result.as_ref().ok_or_else(|| missing("write"))? {
R::Success(success) => Ok((
success.file_content_after_write.clone().unwrap_or_else(|| {
format!(
"write success path={} lines={}",
success.path, success.lines_created
)
}),
false,
)),
R::PermissionDenied(value) => Ok((value.error.clone(), true)),
R::NoSpace(value) => Ok((format!("no space left: {}", value.path), true)),
R::Error(value) => Ok((value.error.clone(), true)),
R::Rejected(value) => Ok((value.reason.clone(), true)),
}
}
fn delete(value: &pb::DeleteResult) -> Result<(String, bool)> {
use pb::delete_result::Result as R;
match value.result.as_ref().ok_or_else(|| missing("delete"))? {
R::Success(value) => Ok((format!("delete success path={}", value.path), false)),
R::FileNotFound(value) => Ok((format!("file not found: {}", value.path), true)),
R::NotFile(value) => Ok((format!("not file: {}", value.path), true)),
R::PermissionDenied(value) => Ok((value.client_visible_error.clone(), true)),
R::FileBusy(value) => Ok((format!("file busy: {}", value.path), true)),
R::Rejected(value) => Ok((value.reason.clone(), true)),
R::Error(value) => Ok((value.error.clone(), true)),
}
}
fn grep(value: &pb::GrepResult) -> Result<(String, bool)> {
use pb::grep_result::Result as R;
match value.result.as_ref().ok_or_else(|| missing("grep"))? {
R::Success(value) => Ok((grep_success(value), false)),
R::Error(value) => Ok((value.error.clone(), true)),
}
}
fn grep_success(value: &pb::GrepSuccess) -> String {
let mut lines = Vec::new();
if let Some(result) = &value.active_editor_result {
grep_union(result, &mut lines);
}
let mut workspaces = value.workspace_results.iter().collect::<Vec<_>>();
workspaces.sort_unstable_by_key(|(name, _)| *name);
for (_, result) in workspaces {
grep_union(result, &mut lines);
}
if lines.is_empty() {
format!(
"No matches found for pattern `{}` in {}",
value.pattern, value.path
)
} else {
lines.join("\n")
}
}
fn grep_union(value: &pb::GrepUnionResult, lines: &mut Vec<String>) {
use pb::grep_union_result::Result as R;
match value.result.as_ref() {
Some(R::Files(value)) => {
lines.extend(value.files.iter().cloned());
grep_truncation(
value.client_truncated,
value.ripgrep_truncated,
value.total_files,
"files",
lines,
);
}
Some(R::Count(value)) => {
lines.extend(
value
.counts
.iter()
.map(|count| format!("{}:{}", count.file, count.count)),
);
grep_truncation(
value.client_truncated,
value.ripgrep_truncated,
value.total_matches,
"matches",
lines,
);
}
Some(R::Content(value)) => {
for file in &value.matches {
lines.extend(file.matches.iter().map(|matched| {
let separator = if matched.is_context_line { '-' } else { ':' };
let truncated = if matched.content_truncated {
" [line truncated]"
} else {
""
};
format!(
"{}{separator}{}{separator}{}{truncated}",
file.file, matched.line_number, matched.content
)
}));
}
grep_truncation(
value.client_truncated,
value.ripgrep_truncated,
value.total_matched_lines,
"matched lines",
lines,
);
}
None => {}
}
}
fn grep_truncation(
client_truncated: bool,
ripgrep_truncated: bool,
total: i32,
unit: &str,
lines: &mut Vec<String>,
) {
if client_truncated || ripgrep_truncated {
lines.push(format!("[Results truncated; {total} total {unit}]"));
}
}
fn diagnostics(value: &pb::DiagnosticsResult) -> Result<(String, bool)> {
use pb::diagnostics_result::Result as R;
match value
.result
.as_ref()
.ok_or_else(|| missing("diagnostics"))?
{
R::Success(value) => Ok((diagnostics_success(value), false)),
R::Error(value) => Ok((value.error.clone(), true)),
R::Rejected(value) => Ok((value.reason.clone(), true)),
R::FileNotFound(value) => Ok((format!("file not found: {}", value.path), true)),
R::PermissionDenied(value) => Ok((format!("permission denied: {}", value.path), true)),
}
}
fn diagnostics_success(value: &pb::DiagnosticsSuccess) -> String {
if value.diagnostics.is_empty() {
return format!("No diagnostics found in {}", value.path);
}
let mut lines = value
.diagnostics
.iter()
.map(|diagnostic| {
let location = diagnostic_location(&value.path, diagnostic.range.as_ref());
let mut labels = vec![diagnostic_severity(diagnostic.severity)];
if !diagnostic.source.is_empty() {
labels.push(diagnostic.source.as_str());
}
if !diagnostic.code.is_empty() {
labels.push(diagnostic.code.as_str());
}
if diagnostic.is_stale {
labels.push("stale");
}
format!(
"{}: [{}] {}",
location,
labels.join(" "),
diagnostic.message
)
})
.collect::<Vec<_>>();
if value.total_diagnostics != value.diagnostics.len() as i32 {
lines.push(format!(
"[Reported {} diagnostics; received {} details]",
value.total_diagnostics,
value.diagnostics.len()
));
}
lines.join("\n")
}
fn diagnostic_location(path: &str, range: Option<&pb::Range>) -> String {
let Some(range) = range else {
return path.into();
};
let Some(start) = &range.start else {
return path.into();
};
let mut location = format!(
"{}:{}:{}",
path,
start.line.saturating_add(1),
start.column.saturating_add(1)
);
if let Some(end) = &range.end {
location.push_str(&format!(
"-{}:{}",
end.line.saturating_add(1),
end.column.saturating_add(1)
));
}
location
}
fn diagnostic_severity(value: i32) -> &'static str {
match pb::DiagnosticSeverity::try_from(value) {
Ok(pb::DiagnosticSeverity::Error) => "error",
Ok(pb::DiagnosticSeverity::Warning) => "warning",
Ok(pb::DiagnosticSeverity::Information) => "information",
Ok(pb::DiagnosticSeverity::Hint) => "hint",
Ok(pb::DiagnosticSeverity::Unspecified) | Err(_) => "diagnostic",
}
}
fn mcp(value: &pb::McpResult) -> Result<(String, bool)> {
use pb::mcp_result::Result as R;
match value.result.as_ref().ok_or_else(|| missing("mcp"))? {
R::Success(value) => Ok((mcp_content(value)?, value.is_error)),
R::Error(value) => Ok((value.error.clone(), true)),
R::Rejected(value) => Ok((value.reason.clone(), true)),
R::PermissionDenied(value) => Ok((value.error.clone(), true)),
R::ToolNotFound(value) => Ok((format!("MCP tool not found: {}", value.name), true)),
R::ServerNotFound(value) => Ok((format!("MCP server not found: {}", value.name), true)),
R::Approved(_) => Err(Error::Protocol("MCP approval is not terminal".into())),
}
}
fn mcp_content(success: &pb::McpSuccess) -> Result<String> {
let mut content = Vec::new();
for item in &success.content {
match item.content.as_ref() {
Some(pb::mcp_tool_result_content_item::Content::Text(text)) => {
if !text.text.is_empty() {
content.push(text.text.clone());
}
if let Some(location) = &text.output_location {
content.push(format!(
"MCP output file: {} ({} bytes, {} lines)",
location.file_path, location.size_bytes, location.line_count
));
}
}
Some(pb::mcp_tool_result_content_item::Content::Image(image)) => content.push(format!(
"MCP image: {} ({} bytes)",
image.mime_type,
image.data.len()
)),
None => {}
}
}
if let Some(structured) = &success.structured_content {
let value = serde_json::Value::Object(
structured
.fields
.iter()
.map(|(key, value)| (key.clone(), super::super::prost_json(value)))
.collect(),
);
content.push(serde_json::to_string_pretty(&value)?);
}
Ok(if content.is_empty() {
"MCP tool completed without content".into()
} else {
content.join("\n\n")
})
}
fn read_mcp(value: &pb::ReadMcpResourceExecResult) -> Result<(String, bool)> {
use pb::read_mcp_resource_exec_result::Result as R;
match value
.result
.as_ref()
.ok_or_else(|| missing("read MCP resource"))?
{
R::Success(value) => Ok((
match value.content.as_ref() {
Some(pb::read_mcp_resource_success::Content::Text(text)) => text.clone(),
Some(pb::read_mcp_resource_success::Content::Blob(blob)) => {
format!("read MCP resource blob={}", blob.len())
}
None => format!("read MCP resource uri={}", value.uri),
},
false,
)),
R::Error(value) => Ok((value.error.clone(), true)),
R::Rejected(value) => Ok((value.reason.clone(), true)),
R::NotFound(value) => Ok((format!("MCP resource not found: {}", value.uri), true)),
}
}
fn task(value: &pb::SubagentResult, call: &ToolCall) -> Result<(String, bool)> {
use pb::subagent_result::Result as R;
match value.result.as_ref().ok_or_else(|| missing("subagent"))? {
R::Success(value) if creates_subagent(call) => {
let name = call
.arguments
.get("description")
.and_then(serde_json::Value::as_str)
.filter(|name| !name.is_empty())
.ok_or_else(|| Error::Protocol("Task call is missing description".into()))?;
if value.agent_id.is_empty() {
return Err(Error::Protocol("Task result is missing agent_id".into()));
}
let identity = format!("Subagent name: {name}\nSubagent ID: {}", value.agent_id);
let content = value
.final_message
.as_deref()
.filter(|message| !message.is_empty())
.map_or(identity.clone(), |message| {
format!("{identity}\n\n{message}")
});
Ok((content, false))
}
R::Success(value) => Ok((value.final_message.clone().unwrap_or_default(), false)),
R::Error(value) => Ok((value.error.clone(), true)),
}
}
fn creates_subagent(call: &ToolCall) -> bool {
matches!(
call.arguments
.get("resume")
.and_then(serde_json::Value::as_str),
None | Some("self")
)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn grep_output_contains_file_and_match_details() {
let value = pb::GrepResult {
result: Some(pb::grep_result::Result::Success(pb::GrepSuccess {
pattern: "Cursor".into(),
path: "/workspace".into(),
output_mode: "content".into(),
workspace_results: HashMap::from([
(
"workspace-b".into(),
pb::GrepUnionResult {
result: Some(pb::grep_union_result::Result::Files(
pb::GrepFilesResult {
files: vec!["/workspace/Cargo.toml".into()],
total_files: 1,
..Default::default()
},
)),
},
),
(
"workspace-a".into(),
pb::GrepUnionResult {
result: Some(pb::grep_union_result::Result::Content(
pb::GrepContentResult {
matches: vec![pb::GrepFileMatch {
file: "/workspace/README.md".into(),
matches: vec![pb::GrepContentMatch {
line_number: 7,
content: "Cursor BYOK".into(),
..Default::default()
}],
}],
total_lines: 1,
total_matched_lines: 1,
..Default::default()
},
)),
},
),
]),
active_editor_result: None,
})),
};
let (content, is_error) = grep(&value).unwrap();
assert!(!is_error);
assert!(content.contains("/workspace/README.md:7:Cursor BYOK"));
assert!(content.contains("/workspace/Cargo.toml"));
assert!(
content.find("/workspace/README.md").unwrap()
< content.find("/workspace/Cargo.toml").unwrap(),
"workspace map output must be deterministic"
);
}
#[test]
fn diagnostics_output_contains_each_diagnostic_detail() {
let value = pb::DiagnosticsResult {
result: Some(pb::diagnostics_result::Result::Success(
pb::DiagnosticsSuccess {
path: "/workspace/src/main.rs".into(),
diagnostics: vec![pb::Diagnostic {
severity: pb::DiagnosticSeverity::Error as i32,
range: Some(pb::Range {
start: Some(pb::Position { line: 4, column: 8 }),
end: Some(pb::Position {
line: 4,
column: 12,
}),
}),
message: "cannot find value `name`".into(),
source: "rustc".into(),
code: "E0425".into(),
is_stale: false,
}],
total_diagnostics: 1,
},
)),
};
let (content, is_error) = diagnostics(&value).unwrap();
assert!(!is_error);
assert!(content.contains("/workspace/src/main.rs:5:9"));
assert!(content.contains("-5:13"));
assert!(content.contains("error"));
assert!(content.contains("rustc"));
assert!(content.contains("E0425"));
assert!(content.contains("cannot find value `name`"));
}
}
fn missing(name: &str) -> Error {
Error::Protocol(format!("{name} returned no result"))
}