Skip to main content

opi_coding_agent/tool/
read.rs

1use std::future::Future;
2use std::path::PathBuf;
3use std::pin::Pin;
4
5use opi_agent::tool::{ExecutionMode, Tool, ToolError, ToolResult};
6use opi_ai::message::{OutputContent, ToolDef};
7use schemars::JsonSchema;
8use serde::Deserialize;
9use tokio_util::sync::CancellationToken;
10
11#[derive(Debug, Deserialize, JsonSchema)]
12pub struct ReadArgs {
13    /// Relative path within workspace to read.
14    pub path: String,
15    /// 1-based line offset (optional, defaults to 1).
16    pub offset: Option<usize>,
17    /// Maximum number of lines to read (optional).
18    pub limit: Option<usize>,
19}
20
21pub struct ReadTool {
22    workspace_root: PathBuf,
23    schema: serde_json::Value,
24}
25
26impl ReadTool {
27    pub fn new(workspace_root: PathBuf) -> Self {
28        let schema = schemars::schema_for!(ReadArgs);
29        Self {
30            workspace_root,
31            schema: serde_json::to_value(&schema).unwrap_or_default(),
32        }
33    }
34}
35
36impl Tool for ReadTool {
37    fn definition(&self) -> ToolDef {
38        ToolDef {
39            name: "read".into(),
40            description: "Read file content with optional line range.".into(),
41            input_schema: self.schema.clone(),
42        }
43    }
44
45    fn execute(
46        &self,
47        _call_id: &str,
48        arguments: serde_json::Value,
49        _signal: CancellationToken,
50        _on_update: Option<opi_agent::tool::UpdateCallback>,
51    ) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send>> {
52        let args: ReadArgs = match serde_json::from_value(arguments) {
53            Ok(a) => a,
54            Err(e) => {
55                return Box::pin(async move {
56                    Ok(ToolResult {
57                        content: vec![OutputContent::Text {
58                            text: format!("invalid arguments: {e}"),
59                        }],
60                        details: None,
61                        is_error: true,
62                        terminate: false,
63                    })
64                });
65            }
66        };
67        let file_path = match super::validate_workspace_path(&self.workspace_root, &args.path) {
68            Ok(p) => p,
69            Err(msg) => {
70                return Box::pin(async move {
71                    Ok(ToolResult {
72                        content: vec![OutputContent::Text { text: msg }],
73                        details: None,
74                        is_error: true,
75                        terminate: false,
76                    })
77                });
78            }
79        };
80        let workspace_root = self.workspace_root.clone();
81        let path_for_display = args.path.clone();
82        Box::pin(async move {
83            let content = match tokio::fs::read_to_string(&file_path).await {
84                Ok(c) => c,
85                Err(e) => {
86                    return Ok(ToolResult {
87                        content: vec![OutputContent::Text {
88                            text: format!("failed to read {}: {e}", file_path.display()),
89                        }],
90                        details: None,
91                        is_error: true,
92                        terminate: false,
93                    });
94                }
95            };
96
97            let lines: Vec<&str> = content.lines().collect();
98            let offset = args.offset.unwrap_or(1).saturating_sub(1);
99            let offset = offset.min(lines.len());
100            let selected: Vec<&str> = if let Some(limit) = args.limit {
101                lines[offset..].iter().take(limit).copied().collect()
102            } else {
103                lines[offset..].to_vec()
104            };
105
106            let output = selected.join("\n");
107            let details = serde_json::json!({
108                "workspace_root": workspace_root.to_string_lossy(),
109                "path": path_for_display,
110            });
111
112            let text = format!("{}\n{}", file_path.display(), output);
113
114            Ok(ToolResult {
115                content: vec![OutputContent::Text { text }],
116                details: Some(details),
117                is_error: false,
118                terminate: false,
119            })
120        })
121    }
122
123    fn execution_mode(&self) -> ExecutionMode {
124        ExecutionMode::Parallel
125    }
126}