agent_workspace_contract/
workspace.rs1use super::*;
2
3pub const WORKSPACE_MISSING_FILE_REVISION: &str = "missing";
5
6pub fn revision_matches(expected: &str, actual: Option<&str>) -> bool {
7 if expected == WORKSPACE_MISSING_FILE_REVISION {
8 actual.is_none()
9 } else {
10 actual == Some(expected)
11 }
12}
13
14#[async_trait]
16pub trait Workspace: Send + Sync + Debug {
17 fn description(&self) -> String;
18 fn root(&self) -> PathBuf;
19 fn capabilities(&self) -> WorkspaceCapabilities {
25 WorkspaceCapabilities::default()
26 }
27 fn resolved_backend_id(&self) -> Option<ResolvedBackendId> {
28 None
29 }
30 fn supports_change_sets(&self) -> bool {
32 false
33 }
34
35 async fn apply_change_set(
36 &self,
37 _request: WorkspaceChangeSetRequest,
38 ) -> Result<WorkspaceChangeSet> {
39 anyhow::bail!("workspace change sets are not supported by this workspace")
40 }
41
42 async fn get_change_set(&self, _id: &str) -> Result<WorkspaceChangeSet> {
43 anyhow::bail!("workspace change sets are not supported by this workspace")
44 }
45
46 async fn revert_change_set(
47 &self,
48 _id: &str,
49 _conversation_id: Option<String>,
50 ) -> Result<WorkspaceChangeSet> {
51 anyhow::bail!("workspace change sets are not supported by this workspace")
52 }
53
54 async fn read_file(&self, path: &str) -> Result<String>;
55 async fn read_file_tail(&self, path: &str, max_bytes: usize) -> Result<String> {
61 if max_bytes == 0 {
62 return Ok(String::new());
63 }
64 let mut value = self.read_file(path).await?;
65 if value.len() <= max_bytes {
66 return Ok(value);
67 }
68 let mut start = value.len() - max_bytes;
69 while !value.is_char_boundary(start) {
70 start += 1;
71 }
72 value.drain(..start);
73 Ok(value)
74 }
75 async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>> {
76 Ok(self.read_file(path).await?.into_bytes())
77 }
78 async fn write_file(&self, path: &str, content: &str) -> Result<()>;
79 async fn write_file_bytes(&self, path: &str, content: &[u8]) -> Result<()> {
80 let content = std::str::from_utf8(content)?;
81 self.write_file(path, content).await
82 }
83 async fn file_revision(&self, path: &str) -> Result<Option<String>> {
85 if !self.exists(path).await? {
86 return Ok(None);
87 }
88 Ok(Some(content_revision(&self.read_file_bytes(path).await?)))
89 }
90 async fn read_file_versioned(&self, path: &str) -> Result<VersionedFile> {
91 let bytes = self.read_file_bytes(path).await?;
92 let revision = content_revision(&bytes);
93 Ok(VersionedFile { bytes, revision })
94 }
95 async fn write_file_if_match(
98 &self,
99 path: &str,
100 content: &[u8],
101 if_match: Option<&str>,
102 ) -> Result<String> {
103 if let Some(expected) = if_match {
104 let actual = self.file_revision(path).await?;
105 if !revision_matches(expected, actual.as_deref()) {
106 return Err(WorkspaceVersionConflict {
107 expected: expected.to_string(),
108 actual,
109 }
110 .into());
111 }
112 }
113 self.write_file_bytes(path, content).await?;
114 Ok(content_revision(content))
115 }
116 async fn write_file_chunk(
118 &self,
119 _path: &str,
120 _offset: u64,
121 _content: bytes::Bytes,
122 _final_chunk: bool,
123 ) -> Result<()> {
124 anyhow::bail!("resumable uploads are not supported by this workspace")
125 }
126 async fn create_dir_all(&self, path: &str) -> Result<()>;
129 async fn remove_file(&self, path: &str) -> Result<()>;
130 async fn remove_file_if_match(&self, path: &str, if_match: &str) -> Result<()> {
133 let actual = self.file_revision(path).await?;
134 if !revision_matches(if_match, actual.as_deref()) {
135 return Err(WorkspaceVersionConflict {
136 expected: if_match.to_string(),
137 actual,
138 }
139 .into());
140 }
141 self.remove_file(path).await
142 }
143 async fn exists(&self, path: &str) -> Result<bool>;
144 async fn is_dir(&self, path: &str) -> Result<bool>;
145 async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>>;
146 async fn file_size(&self, path: &str) -> Result<Option<u64>> {
147 let path = std::path::Path::new(path);
148 let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
149 return Ok(None);
150 };
151 let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
152 Ok(self
153 .list_dir(parent.to_string_lossy().as_ref())
154 .await?
155 .into_iter()
156 .find(|entry| !entry.is_dir && entry.name == name)
157 .and_then(|entry| entry.size))
158 }
159 async fn list_dir_with_options(
160 &self,
161 path: &str,
162 _options: FileVisibilityOptions,
163 ) -> Result<Vec<DirEntry>> {
164 self.list_dir(path).await
165 }
166 async fn walk_tree(&self, path: &str, max_depth: usize) -> Result<Vec<String>>;
167 async fn walk_tree_with_options(
168 &self,
169 path: &str,
170 max_depth: usize,
171 _options: FileVisibilityOptions,
172 ) -> Result<Vec<String>> {
173 self.walk_tree(path, max_depth).await
174 }
175 async fn find_files(&self, pattern: &str, path: &str) -> Result<Vec<String>>;
176 async fn find_files_with_options(
177 &self,
178 pattern: &str,
179 path: &str,
180 _options: FileVisibilityOptions,
181 ) -> Result<Vec<String>> {
182 self.find_files(pattern, path).await
183 }
184 async fn grep(
185 &self,
186 pattern: &str,
187 path: &str,
188 include: Option<&str>,
189 ) -> Result<Vec<GrepMatch>>;
190 async fn grep_with_options(
191 &self,
192 pattern: &str,
193 path: &str,
194 include: Option<&str>,
195 _options: FileVisibilityOptions,
196 ) -> Result<Vec<GrepMatch>> {
197 self.grep(pattern, path, include).await
198 }
199
200 async fn exec(&self, command: &str, cwd: Option<&str>) -> Result<CmdOutput>;
201
202 async fn resource_usage(&self) -> Result<WorkspaceResourceUsage> {
208 Ok(WorkspaceResourceUsage::unavailable(
209 self.description(),
210 self.root(),
211 self.resolved_backend_id().map(|value| value.0),
212 "providerResourceUsageUnavailable",
213 ))
214 }
215
216 async fn exec_detached(
220 &self,
221 command: &str,
222 cwd: Option<&str>,
223 _timeout_secs: u64,
224 ) -> Result<CmdOutput> {
225 let _ = (command, cwd);
226 anyhow::bail!("detached command execution requires a provider-native executor")
227 }
228
229 async fn port_url(&self, port: u16) -> Result<PortUrl> {
230 anyhow::bail!(
231 "preview URL lookup is not supported for {} on port {}",
232 self.description(),
233 port
234 )
235 }
236
237 async fn port_url_with_ttl(&self, port: u16, _ttl_seconds: u64) -> Result<PortUrl> {
240 self.port_url(port).await
241 }
242
243 async fn start_static_preview(&self, directory: &str) -> Result<StaticPreviewServer> {
249 let _ = directory;
250 anyhow::bail!("static preview requires a provider-native preview adapter")
251 }
252
253 async fn static_preview_ready(&self, server: &StaticPreviewServer) -> Result<bool> {
257 let _ = server;
258 Ok(false)
259 }
260
261 async fn stop_static_preview(&self, id: &str) -> Result<()> {
262 let _ = id;
263 anyhow::bail!("static preview requires a provider-native preview adapter")
264 }
265
266 async fn keep_alive(&self) -> Result<()> {
267 Ok(())
268 }
269
270 async fn init(&self) -> Result<()> {
271 Ok(())
272 }
273
274 async fn cleanup(&self) -> Result<()> {
275 Ok(())
276 }
277}