1use std::sync::Arc;
4
5use schemars::JsonSchema;
6use serde::Deserialize;
7use serde_json::Value;
8use sgr_agent_core::agent_tool::{Tool, ToolError, ToolOutput, parse_args};
9use sgr_agent_core::context::AgentContext;
10use sgr_agent_core::schema::json_schema_for;
11
12use crate::backend::FileBackend;
13use crate::helpers::backend_err;
14
15pub struct CopyTool<B: FileBackend>(pub Arc<B>);
16
17#[derive(Deserialize, JsonSchema)]
18struct CopyArgs {
19 source: String,
21 target: String,
23}
24
25#[async_trait::async_trait]
26impl<B: FileBackend> Tool for CopyTool<B> {
27 fn name(&self) -> &str {
28 "copy_file"
29 }
30 fn description(&self) -> &str {
31 "Copy a file byte-for-byte. Use instead of read+write when content must be preserved verbatim (long docs, invoices, migration)"
32 }
33 fn parameters_schema(&self) -> Value {
34 json_schema_for::<CopyArgs>()
35 }
36 async fn execute(&self, args: Value, _ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
37 let a: CopyArgs = parse_args(&args)?;
38 let content = self
39 .0
40 .read(&a.source, false, 0, 0)
41 .await
42 .map_err(backend_err)?;
43 let body = if content.starts_with("$ ") {
45 content
46 .find('\n')
47 .map(|i| &content[i + 1..])
48 .unwrap_or(&content)
49 } else {
50 &content
51 };
52 self.0
53 .write(&a.target, body, 0, 0)
54 .await
55 .map_err(backend_err)?;
56 let bytes = body.len();
57 Ok(ToolOutput::text(format!(
58 "Copied {} → {} ({} bytes)",
59 a.source, a.target, bytes
60 )))
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::mock_fs::MockFs;
68 use sgr_agent_core::agent_tool::Tool;
69
70 #[tokio::test]
71 async fn test_copy_file() {
72 let fs = Arc::new(MockFs::new());
73 fs.add_file("src.md", "# Hello\n\nLong content here...");
74 let tool = CopyTool(fs.clone());
75 let mut ctx = AgentContext::new();
76 let result = tool
77 .execute(
78 serde_json::json!({"source": "src.md", "target": "dst.md"}),
79 &mut ctx,
80 )
81 .await
82 .unwrap();
83 assert!(result.content.contains("Copied src.md → dst.md"));
84 assert!(
85 fs.content("dst.md")
86 .unwrap()
87 .starts_with("# Hello\n\nLong content here...")
88 );
89 assert!(fs.exists("src.md"));
91 }
92
93 #[tokio::test]
94 async fn test_copy_in_place() {
95 let fs = Arc::new(MockFs::new());
96 fs.add_file("doc.md", "original content");
97 let tool = CopyTool(fs.clone());
98 let mut ctx = AgentContext::new();
99 let result = tool
100 .execute(
101 serde_json::json!({"source": "doc.md", "target": "doc.md"}),
102 &mut ctx,
103 )
104 .await
105 .unwrap();
106 assert!(result.content.contains("Copied doc.md → doc.md"));
107 assert!(
108 fs.content("doc.md")
109 .unwrap()
110 .starts_with("original content")
111 );
112 }
113}