use std::path::PathBuf;
use std::sync::Arc;
use rig_core::completion::ToolAnnotations;
use rig_core::tool::{
ChangeKind, FileChange, FilesChanged, Scope, Tool, ToolContext, ToolExecutionError, Touches,
args_schema,
};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::fs::{Previous, io_error};
use crate::workspace::{Local, Workspace};
use crate::{MAX_FILE_BYTES, Seen, blocking};
#[derive(Clone, Debug)]
pub struct Write {
pub workspace: Arc<dyn Workspace>,
pub base: Option<PathBuf>,
pub confine: bool,
pub seen: Seen,
}
impl Default for Write {
fn default() -> Self {
Self {
workspace: Arc::new(Local),
base: None,
confine: false,
seen: Seen::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WriteArgs {
pub path: String,
pub content: String,
}
impl WriteArgs {
pub fn new(path: impl Into<String>, content: impl Into<String>) -> Self {
Self {
path: path.into(),
content: content.into(),
}
}
}
impl Tool for Write {
const NAME: &'static str = "write";
type Args = WriteArgs;
type Output = String;
type Error = ToolExecutionError;
fn description(&self) -> String {
"Write a file with the given content, replacing it if it exists and creating parent \
directories. Use it for new files and complete rewrites only; change part of a file \
with `edit`."
.to_owned()
}
fn parameters(&self) -> serde_json::Value {
args_schema::<WriteArgs>()
}
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations::default()
.with_destructive(true)
.with_idempotent(true)
.with_open_world(false)
}
fn footprint(&self, args: &WriteArgs) -> Touches {
Touches::Writes(Scope::Paths(vec![
self.workspace.resolve(self.base.as_deref(), &args.path),
]))
}
async fn call(
&self,
context: &mut ToolContext,
args: WriteArgs,
) -> Result<String, ToolExecutionError> {
let resolved = self
.workspace
.place(self.base.as_deref(), self.confine, &args.path)?;
let workspace = self.workspace.clone();
let seen = self.seen.clone();
let (output, change) = blocking(move || {
let path = resolved.to_string_lossy();
let before = workspace
.replace(&resolved, args.content.as_bytes(), MAX_FILE_BYTES)
.map_err(|error| {
seen.forget(&resolved);
io_error(&path, error)
})?;
seen.saw_whole(&resolved, &args.content);
let before = Previous::read(before);
let change = Previous::change(before, &path, Some(args.content.as_bytes()));
Ok::<_, ToolExecutionError>((wrote(&args.path, &change), change))
})
.await?;
context.insert_result(FilesChanged(vec![change]))?;
Ok(output)
}
}
fn wrote(path: &str, change: &FileChange) -> String {
let had = change.before.as_ref().and_then(|before| before.lines);
match (change.kind, had) {
(ChangeKind::Created, _) => format!("Created {path}."),
(_, Some(1)) => format!("Replaced {path}, which had 1 line."),
(_, Some(lines)) => format!("Replaced {path}, which had {lines} lines."),
(_, None) => format!("Replaced {path}."),
}
}
#[cfg(test)]
mod tests;