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