use std::fs;
use std::path::Path as StdPath;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use handled::SError;
use serde::{Deserialize, Serialize};
use crate::config::TOOL_PROTOCOL_VERSION;
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolRequestEnvelope {
pub protocol_version: u32,
pub request_id: String,
pub tool: ToolRequestTool,
pub invocation: ToolRequestInvocation,
pub agent: ToolRequestAgent,
pub workspace: ToolRequestWorkspace,
pub files: ToolRequestFiles,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolRequestTool {
pub id: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolRequestInvocation {
pub tool_use_id: String,
pub input: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolRequestAgent {
pub id: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolRequestWorkspace {
pub root: String,
pub cwd: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolRequestFiles {
pub scratch_dir: String,
pub temp_dir: String,
pub result_file: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolResultEnvelope {
pub protocol_version: u32,
pub request_id: String,
pub ok: bool,
pub output: Option<ToolResultOutput>,
pub error: Option<ToolResultError>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolResultOutput {
pub kind: String,
pub text: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolResultError {
pub code: Option<String>,
pub message: Option<String>,
}
fn protocol_error(code: &str, message: &str) -> SError {
SError::new("tool-protocol")
.with_code(code)
.with_message(message)
}
pub fn extract_tool_output(
display_name: &str,
request_id: &str,
result: ToolResultEnvelope,
) -> Result<String, String> {
if result.protocol_version != TOOL_PROTOCOL_VERSION {
return Err(format!(
"tool '{display_name}' protocol error: unsupported result protocol version {0}",
result.protocol_version
));
}
if result.request_id != request_id {
return Err(format!(
"tool '{display_name}' protocol error: request_id mismatch (expected {request_id}, got {result_request_id})",
result_request_id = result.request_id
));
}
if result.ok {
let Some(output) = result.output else {
return Err(format!(
"tool '{display_name}' protocol error: missing success output"
));
};
if output.kind != "text" {
return Err(format!(
"tool '{display_name}' protocol error: unsupported output kind '{0}'",
output.kind
));
}
let Some(text) = output.text else {
return Err(format!(
"tool '{display_name}' protocol error: missing output.text"
));
};
return Ok(text);
}
let Some(error) = result.error else {
return Err(format!(
"tool '{display_name}' protocol error: missing error object"
));
};
let Some(message) = error.message else {
return Err(format!(
"tool '{display_name}' protocol error: missing error.message"
));
};
let _ = error.code;
Err(message)
}
pub fn write_json_file(path: &StdPath, value: &impl Serialize) -> Result<(), SError> {
let path_display = path.to_string_lossy();
let payload = serde_json::to_vec_pretty(value).map_err(|err| {
protocol_error("json_serialize_error", "failed to serialize JSON file")
.with_string_field("path", path_display.as_ref())
.with_string_field("cause", &err.to_string())
})?;
fs::write(path, payload).map_err(|err| {
protocol_error("io_error", "failed to write JSON file")
.with_string_field("path", path_display.as_ref())
.with_string_field("cause", &err.to_string())
})
}
pub fn read_tool_result(path: &StdPath) -> Result<ToolResultEnvelope, SError> {
let path_display = path.to_string_lossy();
let payload = fs::read_to_string(path).map_err(|err| {
protocol_error("io_error", "failed to read tool result file")
.with_string_field("path", path_display.as_ref())
.with_string_field("cause", &err.to_string())
})?;
serde_json::from_str(&payload).map_err(|err| {
protocol_error(
"invalid_tool_result_json",
"failed to parse tool result file",
)
.with_string_field("path", path_display.as_ref())
.with_string_field("cause", &err.to_string())
})
}
pub fn next_request_id() -> String {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let sequence = NEXT_ID.fetch_add(1, Ordering::Relaxed);
format!("sidreq_{timestamp}_{}_{}", std::process::id(), sequence)
}