use super::file::{
clear_file_snapshot, is_file_stale, preserve_line_endings, read_file_with_encoding,
resolve_safety_config, validate_tool_path,
};
use super::Tool;
use crate::api::ApiClient;
use crate::config::SafetyConfig;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use regex::Regex;
use serde_json::Value;
use std::path::Path;
use std::sync::Arc;
use tokio::fs;
fn detect_line_ending_fim(text: &str) -> &'static str {
if text.contains("\r\n") {
"\r\n"
} else {
"\n"
}
}
const FIM_INSTRUCTION_MAX_LEN: usize = 500;
fn sanitize_fim_instruction(raw: &str) -> String {
let fim_tokens: &[&str] = &[
"<|fim_prefix|>",
"<|fim_suffix|>",
"<|fim_middle|>",
"<|endoftext|>",
"<|file_separator|>",
"<|im_start|>",
"<|im_end|>",
"<|pad|>",
];
let mut sanitized = raw.to_string();
for token in fim_tokens {
let pattern = regex::escape(token);
if let Ok(re) = Regex::new(&format!("(?i){}", pattern)) {
sanitized = re.replace_all(&sanitized, "").to_string();
}
}
let injection_patterns: &[&str] = &[
r"(?i)ignore\s+(all\s+)?previous",
r"(?i)disregard\s+(all\s+)?(previous\s+)?instructions?",
r"(?i)forget\s+(all\s+)?(previous\s+)?instructions?",
r"(?i)override\s+(all\s+)?(previous\s+)?instructions?",
r"(?i)system\s*:",
r"(?i)user\s*:",
r"(?i)assistant\s*:",
r"(?i)new\s+instructions?\s*:",
];
for pat in injection_patterns {
if let Ok(re) = Regex::new(pat) {
sanitized = re.replace_all(&sanitized, "").to_string();
}
}
if let Ok(re) = Regex::new(r"\s{2,}") {
sanitized = re.replace_all(&sanitized, " ").to_string();
}
let sanitized = sanitized.trim().to_string();
if sanitized.len() > FIM_INSTRUCTION_MAX_LEN {
let mut end = FIM_INSTRUCTION_MAX_LEN;
while !sanitized.is_char_boundary(end) && end > 0 {
end -= 1;
}
sanitized[..end].to_string()
} else {
sanitized
}
}
pub struct FileFimEdit {
client: Arc<ApiClient>,
pub safety_config: Option<SafetyConfig>,
}
impl FileFimEdit {
pub fn new(client: Arc<ApiClient>) -> Self {
Self {
client,
safety_config: None,
}
}
pub fn with_safety_config(client: Arc<ApiClient>, config: SafetyConfig) -> Self {
Self {
client,
safety_config: Some(config),
}
}
}
#[async_trait]
impl Tool for FileFimEdit {
fn name(&self) -> &str {
"file_fim_edit"
}
fn description(&self) -> &str {
"Use intelligent Fill-in-the-Middle (FIM) to replace a block of code. Provide path, start_line, and end_line of the block to replace, and the instruction of what should go there. The AI will intelligently generate the middle part based on context."
}
fn schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {"type": "string"},
"start_line": {"type": "integer", "description": "1-based starting line number to replace"},
"end_line": {"type": "integer", "description": "1-based ending line number to replace"},
"instruction": {"type": "string", "description": "What should the model generate to replace this block?"}
},
"required": ["path", "start_line", "end_line", "instruction"]
})
}
async fn execute(&self, args: Value) -> Result<Value> {
let path = args["path"]
.as_str()
.ok_or_else(|| anyhow!("Missing path"))?;
let start_line = args["start_line"]
.as_u64()
.ok_or_else(|| anyhow!("Missing start_line"))? as usize;
let end_line = args["end_line"]
.as_u64()
.ok_or_else(|| anyhow!("Missing end_line"))? as usize;
let raw_instruction = args["instruction"]
.as_str()
.ok_or_else(|| anyhow!("Missing instruction"))?;
let instruction = sanitize_fim_instruction(raw_instruction);
let safety = resolve_safety_config(self.safety_config.as_ref());
validate_tool_path(path, &safety)?;
if let Some(true) = is_file_stale(path) {
return Err(anyhow!(
"File {} changed on disk since you last read it. Re-read the file and try again.",
path
));
}
let (content, _) = read_file_with_encoding(Path::new(path)).await?;
let line_ending = detect_line_ending_fim(&content);
let lines: Vec<&str> = content.lines().collect();
if start_line == 0
|| start_line > lines.len()
|| end_line < start_line
|| end_line > lines.len()
{
return Err(anyhow!("Invalid line range"));
}
let prefix = lines[..start_line - 1].join("\n");
let suffix = lines[end_line..].join("\n");
let prompt = format!(
"[SELFWARE_FIM_METADATA_BEGIN]\ninstruction={}\n[SELFWARE_FIM_METADATA_END]\n<|fim_prefix|>{}\n<|fim_suffix|>{}\n<|fim_middle|>",
instruction, prefix, suffix
);
let response = self
.client
.completion(
&prompt,
Some(2048),
Some(vec![
"<|file_separator|>".to_string(),
"<|endoftext|>".to_string(),
]),
)
.await?;
let middle = response
.choices
.first()
.map(|c| c.text.clone())
.unwrap_or_default();
let trimmed = middle.trim();
if trimmed.is_empty() {
return Err(anyhow!(
"FIM generated empty output \u{2014} refusing to write. \
The model may not have understood the instruction."
));
}
let new_content = format!("{}{}{}", prefix, middle, suffix);
if path.ends_with(".rs") {
use tokio::process::Command;
let check = Command::new("rustfmt")
.args(["--edition", "2021", "--check"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn();
if let Ok(mut child) = check {
if let Some(ref mut stdin) = child.stdin {
use tokio::io::AsyncWriteExt;
let _ = stdin.write_all(new_content.as_bytes()).await;
}
if let Ok(status) = child.wait().await {
if status.code().unwrap_or(0) >= 2 {
return Err(anyhow!(
"FIM-generated code has Rust syntax errors (rustfmt \
exit code {}). Refusing to write to prevent corruption.",
status.code().unwrap_or(-1)
));
}
}
}
}
let backup_path = format!("{}.fim-backup", path);
if let Err(e) = fs::copy(path, &backup_path).await {
tracing::debug!("Could not create FIM backup: {}", e);
}
let final_content = preserve_line_endings(&new_content, line_ending);
fs::write(path, &final_content).await?;
clear_file_snapshot(path);
Ok(serde_json::json!({
"status": "success",
"message": format!("Successfully replaced lines {}-{} using FIM.", start_line, end_line),
"backup": backup_path
}))
}
}
#[cfg(test)]
#[path = "../../tests/unit/tools/fim/fim_test.rs"]
mod tests;