opi_coding_agent/tool/
read.rs1use 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 pub path: String,
15 pub offset: Option<usize>,
17 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}