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 resolved_backend_id(&self) -> Option<ResolvedBackendId> {
20 None
21 }
22 fn supports_change_sets(&self) -> bool {
24 false
25 }
26
27 async fn apply_change_set(
28 &self,
29 _request: WorkspaceChangeSetRequest,
30 ) -> Result<WorkspaceChangeSet> {
31 anyhow::bail!("workspace change sets are not supported by this workspace")
32 }
33
34 async fn get_change_set(&self, _id: &str) -> Result<WorkspaceChangeSet> {
35 anyhow::bail!("workspace change sets are not supported by this workspace")
36 }
37
38 async fn revert_change_set(
39 &self,
40 _id: &str,
41 _conversation_id: Option<String>,
42 ) -> Result<WorkspaceChangeSet> {
43 anyhow::bail!("workspace change sets are not supported by this workspace")
44 }
45
46 async fn read_file(&self, path: &str) -> Result<String>;
47 async fn read_file_tail(&self, path: &str, max_bytes: usize) -> Result<String> {
53 if max_bytes == 0 {
54 return Ok(String::new());
55 }
56 let mut value = self.read_file(path).await?;
57 if value.len() <= max_bytes {
58 return Ok(value);
59 }
60 let mut start = value.len() - max_bytes;
61 while !value.is_char_boundary(start) {
62 start += 1;
63 }
64 value.drain(..start);
65 Ok(value)
66 }
67 async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>> {
68 Ok(self.read_file(path).await?.into_bytes())
69 }
70 async fn write_file(&self, path: &str, content: &str) -> Result<()>;
71 async fn write_file_bytes(&self, path: &str, content: &[u8]) -> Result<()> {
72 let content = std::str::from_utf8(content)?;
73 self.write_file(path, content).await
74 }
75 async fn file_revision(&self, path: &str) -> Result<Option<String>> {
77 if !self.exists(path).await? {
78 return Ok(None);
79 }
80 Ok(Some(content_revision(&self.read_file_bytes(path).await?)))
81 }
82 async fn read_file_versioned(&self, path: &str) -> Result<VersionedFile> {
83 let bytes = self.read_file_bytes(path).await?;
84 let revision = content_revision(&bytes);
85 Ok(VersionedFile { bytes, revision })
86 }
87 async fn write_file_if_match(
90 &self,
91 path: &str,
92 content: &[u8],
93 if_match: Option<&str>,
94 ) -> Result<String> {
95 if let Some(expected) = if_match {
96 let actual = self.file_revision(path).await?;
97 if !revision_matches(expected, actual.as_deref()) {
98 return Err(WorkspaceVersionConflict {
99 expected: expected.to_string(),
100 actual,
101 }
102 .into());
103 }
104 }
105 self.write_file_bytes(path, content).await?;
106 Ok(content_revision(content))
107 }
108 async fn write_file_chunk(
110 &self,
111 _path: &str,
112 _offset: u64,
113 _content: bytes::Bytes,
114 _final_chunk: bool,
115 ) -> Result<()> {
116 anyhow::bail!("resumable uploads are not supported by this workspace")
117 }
118 async fn create_dir_all(&self, path: &str) -> Result<()>;
121 async fn remove_file(&self, path: &str) -> Result<()>;
122 async fn remove_file_if_match(&self, path: &str, if_match: &str) -> Result<()> {
125 let actual = self.file_revision(path).await?;
126 if !revision_matches(if_match, actual.as_deref()) {
127 return Err(WorkspaceVersionConflict {
128 expected: if_match.to_string(),
129 actual,
130 }
131 .into());
132 }
133 self.remove_file(path).await
134 }
135 async fn exists(&self, path: &str) -> Result<bool>;
136 async fn is_dir(&self, path: &str) -> Result<bool>;
137 async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>>;
138 async fn file_size(&self, path: &str) -> Result<Option<u64>> {
139 let path = std::path::Path::new(path);
140 let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
141 return Ok(None);
142 };
143 let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
144 Ok(self
145 .list_dir(parent.to_string_lossy().as_ref())
146 .await?
147 .into_iter()
148 .find(|entry| !entry.is_dir && entry.name == name)
149 .and_then(|entry| entry.size))
150 }
151 async fn list_dir_with_options(
152 &self,
153 path: &str,
154 _options: FileVisibilityOptions,
155 ) -> Result<Vec<DirEntry>> {
156 self.list_dir(path).await
157 }
158 async fn walk_tree(&self, path: &str, max_depth: usize) -> Result<Vec<String>>;
159 async fn walk_tree_with_options(
160 &self,
161 path: &str,
162 max_depth: usize,
163 _options: FileVisibilityOptions,
164 ) -> Result<Vec<String>> {
165 self.walk_tree(path, max_depth).await
166 }
167 async fn find_files(&self, pattern: &str, path: &str) -> Result<Vec<String>>;
168 async fn find_files_with_options(
169 &self,
170 pattern: &str,
171 path: &str,
172 _options: FileVisibilityOptions,
173 ) -> Result<Vec<String>> {
174 self.find_files(pattern, path).await
175 }
176 async fn grep(
177 &self,
178 pattern: &str,
179 path: &str,
180 include: Option<&str>,
181 ) -> Result<Vec<GrepMatch>>;
182 async fn grep_with_options(
183 &self,
184 pattern: &str,
185 path: &str,
186 include: Option<&str>,
187 _options: FileVisibilityOptions,
188 ) -> Result<Vec<GrepMatch>> {
189 self.grep(pattern, path, include).await
190 }
191
192 async fn exec(&self, command: &str, cwd: Option<&str>) -> Result<CmdOutput>;
193
194 async fn resource_usage(&self) -> Result<WorkspaceResourceUsage> {
200 Ok(WorkspaceResourceUsage::unavailable(
201 self.description(),
202 self.root(),
203 self.resolved_backend_id().map(|value| value.0),
204 "providerResourceUsageUnavailable",
205 ))
206 }
207
208 async fn exec_detached(
212 &self,
213 command: &str,
214 cwd: Option<&str>,
215 _timeout_secs: u64,
216 ) -> Result<CmdOutput> {
217 let _ = (command, cwd);
218 anyhow::bail!("detached command execution requires a provider-native executor")
219 }
220
221 async fn port_url(&self, port: u16) -> Result<PortUrl> {
222 anyhow::bail!(
223 "preview URL lookup is not supported for {} on port {}",
224 self.description(),
225 port
226 )
227 }
228
229 async fn port_url_with_ttl(&self, port: u16, _ttl_seconds: u64) -> Result<PortUrl> {
232 self.port_url(port).await
233 }
234
235 async fn start_static_preview(&self, directory: &str) -> Result<StaticPreviewServer> {
241 let _ = directory;
242 anyhow::bail!("static preview requires a provider-native preview adapter")
243 }
244
245 async fn static_preview_ready(&self, server: &StaticPreviewServer) -> Result<bool> {
249 let _ = server;
250 Ok(false)
251 }
252
253 async fn stop_static_preview(&self, id: &str) -> Result<()> {
254 let _ = id;
255 anyhow::bail!("static preview requires a provider-native preview adapter")
256 }
257
258 async fn keep_alive(&self) -> Result<()> {
259 Ok(())
260 }
261
262 async fn init(&self) -> Result<()> {
263 Ok(())
264 }
265
266 async fn cleanup(&self) -> Result<()> {
267 Ok(())
268 }
269}