1use futures::FutureExt;
4use futures::future::BoxFuture;
5use oino_agent::{
6 AgentTool, AgentToolResult, ToolDefinitionMeta, ToolError, ToolInvocation, UpdateSink,
7};
8use tokio_util::sync::CancellationToken;
9
10use crate::config::ToolConfig;
11use crate::info::write_info;
12use crate::path_utils::resolve_to_cwd;
13use crate::{aborted, args, node_path};
14
15#[derive(Debug)]
17pub struct WriteTool {
18 meta: ToolDefinitionMeta,
19 config: ToolConfig,
20}
21
22impl WriteTool {
23 #[must_use]
25 pub fn new(config: ToolConfig) -> Self {
26 Self {
27 meta: write_info().definition_meta(),
28 config,
29 }
30 }
31}
32
33impl AgentTool for WriteTool {
34 fn meta(&self) -> &ToolDefinitionMeta {
35 &self.meta
36 }
37
38 fn execute(
39 &self,
40 call: ToolInvocation,
41 _updates: UpdateSink,
42 cancel: CancellationToken,
43 ) -> BoxFuture<'static, Result<AgentToolResult, ToolError>> {
44 let config = self.config.clone();
45 async move { run(&config, &call, &cancel).await }.boxed()
46 }
47}
48
49async fn run(
50 config: &ToolConfig,
51 call: &ToolInvocation,
52 cancel: &CancellationToken,
53) -> Result<AgentToolResult, ToolError> {
54 let path = args::required_str(&call.args, "path")?;
55 let content = args::required_str(&call.args, "content")?;
56 let absolute = resolve_to_cwd(path, &config.effective_cwd())
57 .map_err(|error| ToolError::new(error.to_string()))?;
58 let dir = node_path::dirname(&absolute);
59 let env = config.env.as_ref();
60 let check = || {
63 if cancel.is_cancelled() {
64 Err(aborted())
65 } else {
66 Ok(())
67 }
68 };
69 config
70 .mutation_queue
71 .run(env, &absolute, async {
72 check()?;
73 env.mkdir_p(&dir)
74 .await
75 .map_err(|error| ToolError::new(error.to_string()))?;
76 check()?;
77 env.write_file(&absolute, content.as_bytes())
78 .await
79 .map_err(|error| ToolError::new(error.to_string()))?;
80 check()?;
81 Ok(AgentToolResult::text(format!(
82 "Successfully wrote to {path}"
83 )))
84 })
85 .await
86}