agents_kit/workspace.rs
1//! Workspace contexts, durable snapshots, revision-checked requests, and live handles.
2//!
3//! Context paths describe the backend host, which may differ from the client.
4//! Construction validates lexical confinement without probing that filesystem.
5//! [`WorkspaceBackend`] owns its associated Conversation; close releases runtime
6//! ownership while preserving reopenable records. Use [`crate::AgentBackend`]
7//! for atomic replacement/deletion, supplying the last committed revision.
8
9use std::ffi::OsString;
10use std::fmt;
11use std::future::Future;
12use std::path::{Path, PathBuf};
13use tools_kit::ToolsetBackend;
14
15use providers_kit::ProviderBackend;
16use storage_kit::{DatabaseBackend, StateStoreBackend};
17
18use crate::backends::{codex, custom, opencode};
19use crate::{
20 Conversation, ConversationId, ConversationInfo, IngestKnowledgeRequest, KitError,
21 KnowledgePolicy, ModelSelector, NewConversation, PersonalitySelector, ReasoningEffort,
22 ToolInfo, WorkspaceId,
23};
24
25/// Filesystem access declared for one workspace root.
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum WorkspaceRootAccess {
28 /// Model-visible actions may read but not write beneath the root.
29 ReadOnly,
30 /// Model-visible actions may read and write beneath the root when policy permits.
31 ReadWrite,
32}
33
34/// One normalized absolute backend-host workspace root.
35#[derive(Clone, Debug, Eq, PartialEq)]
36///
37/// ```
38/// use agents_kit::workspace::{WorkspaceRoot, WorkspaceRootAccess};
39/// let value = WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?;
40/// assert_eq!(value.path(), std::path::Path::new("/workspace"));
41/// # Ok::<(), agents_kit::KitError>(())
42/// ```
43pub struct WorkspaceRoot {
44 path: BackendHostPath,
45 access: WorkspaceRootAccess,
46}
47
48#[derive(Clone)]
49struct BackendHostPath {
50 original: PathBuf,
51 anchor: BackendHostPathAnchor,
52 components: Vec<BackendHostPathSegment>,
53}
54
55impl PartialEq for BackendHostPath {
56 fn eq(&self, other: &Self) -> bool {
57 self.anchor == other.anchor && self.components == other.components
58 }
59}
60
61impl Eq for BackendHostPath {}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
64enum BackendHostPathAnchor {
65 Posix,
66 WindowsDrive(u8),
67 WindowsUnc {
68 server: BackendHostPathSegment,
69 share: BackendHostPathSegment,
70 },
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub(crate) enum BackendHostPathFlavor {
75 Posix,
76 Windows,
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
80struct BackendHostPathSegment(Box<[u8]>);
81
82impl fmt::Debug for BackendHostPath {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 formatter
85 .debug_tuple("BackendHostPath")
86 .field(&self.original)
87 .finish()
88 }
89}
90
91impl BackendHostPathSegment {
92 fn new(value: &[u8], label: &str) -> Result<Self, KitError> {
93 if value.is_empty() || value == b"." || value == b".." {
94 return Err(KitError::invalid_input(format!(
95 "{label} must already be lexically normalized"
96 )));
97 }
98 Ok(Self(value.into()))
99 }
100}
101
102impl BackendHostPathAnchor {
103 fn is_windows(&self) -> bool {
104 !matches!(self, Self::Posix)
105 }
106
107 fn flavor(&self) -> BackendHostPathFlavor {
108 if self.is_windows() {
109 BackendHostPathFlavor::Windows
110 } else {
111 BackendHostPathFlavor::Posix
112 }
113 }
114}
115
116impl BackendHostPath {
117 fn parse_absolute(label: &str, original: PathBuf) -> Result<Self, KitError> {
118 let bytes = original.as_os_str().as_encoded_bytes();
119 if bytes.is_empty() || bytes.contains(&0) {
120 return Err(KitError::invalid_input(format!(
121 "{label} must be a nonempty absolute backend-host path"
122 )));
123 }
124
125 let (anchor, components) = if bytes.starts_with(b"\\\\") {
126 let mut components = Self::parse_components(&bytes[2..], true, label)?;
127 if components.len() < 2 || components[0].0.as_ref() == b"?" {
128 return Err(KitError::invalid_input(format!(
129 "{label} must use a normalized UNC share path"
130 )));
131 }
132 let share = components.remove(1);
133 let server = components.remove(0);
134 (
135 BackendHostPathAnchor::WindowsUnc { server, share },
136 components,
137 )
138 } else if bytes.len() >= 3
139 && bytes[0].is_ascii_alphabetic()
140 && bytes[1] == b':'
141 && matches!(bytes[2], b'/' | b'\\')
142 {
143 (
144 BackendHostPathAnchor::WindowsDrive(bytes[0].to_ascii_lowercase()),
145 Self::parse_components(&bytes[3..], true, label)?,
146 )
147 } else if bytes[0] == b'/' && !bytes.starts_with(b"//") {
148 (
149 BackendHostPathAnchor::Posix,
150 Self::parse_components(&bytes[1..], false, label)?,
151 )
152 } else {
153 return Err(KitError::invalid_input(format!(
154 "{label} must be a nonempty absolute backend-host path"
155 )));
156 };
157
158 Ok(Self {
159 original,
160 anchor,
161 components,
162 })
163 }
164
165 fn parse_components(
166 bytes: &[u8],
167 windows: bool,
168 label: &str,
169 ) -> Result<Vec<BackendHostPathSegment>, KitError> {
170 if bytes.is_empty() {
171 return Ok(Vec::new());
172 }
173 let mut components = Vec::new();
174 let mut start = 0;
175 for (index, byte) in bytes.iter().copied().enumerate() {
176 if byte == b'/' || (windows && byte == b'\\') {
177 components.push(BackendHostPathSegment::new(&bytes[start..index], label)?);
178 start = index + 1;
179 }
180 }
181 components.push(BackendHostPathSegment::new(&bytes[start..], label)?);
182 Ok(components)
183 }
184
185 fn validate_portable_relative(path: &Path) -> Result<PathBuf, KitError> {
186 let bytes = path.as_os_str().as_encoded_bytes();
187 if bytes.is_empty()
188 || bytes.contains(&0)
189 || matches!(bytes[0], b'/' | b'\\')
190 || (bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':')
191 {
192 return Err(KitError::invalid_input(
193 "workspace path must be nonempty and relative",
194 ));
195 }
196 if bytes
197 .split(|byte| matches!(*byte, b'/' | b'\\'))
198 .any(|component| component.is_empty() || component == b"." || component == b"..")
199 {
200 return Err(KitError::invalid_input(
201 "workspace path must already be lexically normalized",
202 ));
203 }
204 Ok(path.to_path_buf())
205 }
206
207 fn resolve(&self, relative: &Path) -> Result<Self, KitError> {
208 let relative_components = Self::parse_components(
209 relative.as_os_str().as_encoded_bytes(),
210 self.anchor.is_windows(),
211 "workspace path",
212 )?;
213 let mut raw = OsString::from(self.original.as_os_str());
214 let terminated = raw
215 .as_encoded_bytes()
216 .last()
217 .is_some_and(|byte| *byte == b'/' || (self.anchor.is_windows() && *byte == b'\\'));
218 if !terminated {
219 raw.push(if self.anchor.is_windows() { "\\" } else { "/" });
220 }
221 raw.push(relative.as_os_str());
222 let resolved = Self::parse_absolute("resolved workspace path", PathBuf::from(raw))?;
223 let mut expected = self.components.clone();
224 expected.extend(relative_components);
225 if resolved.anchor != self.anchor || resolved.components != expected {
226 return Err(KitError::invalid_input(
227 "workspace path must remain beneath its backend-host cwd",
228 ));
229 }
230 Ok(resolved)
231 }
232
233 fn relative_native_file(&self, native_path: &Path) -> Result<PathBuf, KitError> {
234 let native = native_path
235 .to_str()
236 .filter(|path| !path.is_empty() && !path.contains('\0'))
237 .ok_or_else(|| KitError::invalid_input("native file path must be nonempty UTF-8"))?;
238 let windows = self.anchor.is_windows();
239 let native = if windows {
240 native.replace('\\', "/")
241 } else {
242 native.to_owned()
243 };
244 let mut parts = native
245 .split('/')
246 .filter(|part| !part.is_empty())
247 .collect::<Vec<_>>();
248 let root = if windows && native.starts_with("//") {
249 if parts.len() < 2 {
250 return Err(KitError::invalid_input("native file path has no UNC share"));
251 }
252 let root = Self::parse_absolute(
253 "native file root",
254 PathBuf::from(format!(r"\\{}\{}", parts[0], parts[1])),
255 )?;
256 parts.drain(..2);
257 Some(root)
258 } else if native.as_bytes().get(1) == Some(&b':')
259 && native.as_bytes()[0].is_ascii_alphabetic()
260 {
261 if !windows || native.as_bytes().get(2) != Some(&b'/') {
262 return Err(KitError::invalid_input(
263 "native file path has an incompatible drive",
264 ));
265 }
266 let root = Self::parse_absolute("native file root", PathBuf::from(&native[..3]))?;
267 parts.remove(0);
268 Some(root)
269 } else if native.starts_with('/') {
270 if windows {
271 return Err(KitError::invalid_input(
272 "native file path requires an explicit Windows root",
273 ));
274 }
275 Some(Self::parse_absolute(
276 "native file root",
277 PathBuf::from("/"),
278 )?)
279 } else {
280 if native.starts_with('\\') {
281 return Err(KitError::invalid_input(
282 "native file path has an incompatible root",
283 ));
284 }
285 None
286 };
287 let mut components = match root {
288 Some(root) if root.anchor == self.anchor => Vec::new(),
289 Some(_) => {
290 return Err(KitError::invalid_input(
291 "native file path has a different root",
292 ));
293 }
294 None => self.components.clone(),
295 };
296 for part in parts {
297 match part {
298 "." => {}
299 ".." => {
300 if components.pop().is_none() {
301 return Err(KitError::invalid_input("native file path escapes its root"));
302 }
303 }
304 part => components.push(BackendHostPathSegment::new(
305 part.as_bytes(),
306 "native file path",
307 )?),
308 }
309 }
310 if !components.starts_with(&self.components) || components.len() == self.components.len() {
311 return Err(KitError::invalid_input(
312 "native file path does not identify a workspace file",
313 ));
314 }
315 let relative = components[self.components.len()..]
316 .iter()
317 .map(|part| {
318 std::str::from_utf8(&part.0)
319 .map_err(|_| KitError::invalid_input("native file path is not UTF-8"))
320 })
321 .collect::<Result<Vec<_>, _>>()?
322 .join("/");
323 Self::validate_portable_relative(Path::new(&relative))
324 }
325
326 fn same_flavor(&self, other: &Self) -> bool {
327 self.anchor.is_windows() == other.anchor.is_windows()
328 }
329
330 fn contains(&self, candidate: &Self) -> bool {
331 self.anchor == candidate.anchor && candidate.components.starts_with(&self.components)
332 }
333
334 fn overlaps(&self, other: &Self) -> bool {
335 self.contains(other) || other.contains(self)
336 }
337
338 fn as_path(&self) -> &Path {
339 &self.original
340 }
341
342 fn into_path_buf(self) -> PathBuf {
343 self.original
344 }
345}
346
347impl WorkspaceRoot {
348 /// Validates and constructs a declared backend-host root.
349 ///
350 /// # Errors
351 /// Returns InvalidInput for a nonabsolute path or invalid backend-host path components.
352 ///
353 /// ```
354 /// use agents_kit::workspace::{WorkspaceRoot, WorkspaceRootAccess};
355 /// let value = WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?;
356 /// assert_eq!(value.path(), std::path::Path::new("/workspace"));
357 /// # Ok::<(), agents_kit::KitError>(())
358 /// ```
359 pub fn new(path: PathBuf, access: WorkspaceRootAccess) -> Result<Self, KitError> {
360 Ok(Self {
361 path: BackendHostPath::parse_absolute("workspace root", path)?,
362 access,
363 })
364 }
365
366 pub(crate) fn relative_native_file(&self, path: &Path) -> Result<PathBuf, KitError> {
367 self.path.relative_native_file(path)
368 }
369
370 /// Returns the normalized absolute backend-host path.
371 #[must_use]
372 ///
373 /// ```
374 /// use agents_kit::workspace::{WorkspaceRoot, WorkspaceRootAccess};
375 /// let value = WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?;
376 /// assert_eq!(value.path(), std::path::Path::new("/workspace"));
377 /// # Ok::<(), agents_kit::KitError>(())
378 /// ```
379 pub fn path(&self) -> &Path {
380 self.path.as_path()
381 }
382
383 /// Returns the declared access level.
384 #[must_use]
385 ///
386 /// ```
387 /// use agents_kit::workspace::{WorkspaceRoot, WorkspaceRootAccess};
388 /// let value = WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?;
389 /// assert_eq!(value.access(), WorkspaceRootAccess::ReadOnly);
390 /// # Ok::<(), agents_kit::KitError>(())
391 /// ```
392 pub fn access(&self) -> WorkspaceRootAccess {
393 self.access
394 }
395}
396
397/// Filesystem confinement selected for every model-visible workspace action.
398#[derive(Clone, Copy, Debug, Eq, PartialEq)]
399pub enum WorkspaceSandboxMode {
400 /// Allows reads only through every installed action path.
401 ReadOnly,
402 /// Allows writes only beneath roots declared read-write.
403 WorkspaceWrite,
404 /// Allows the backend's unrestricted filesystem behavior.
405 FullAccess,
406}
407
408/// Arbitrary network access selected for model-visible workspace actions.
409#[derive(Clone, Copy, Debug, Eq, PartialEq)]
410pub enum WorkspaceNetworkAccess {
411 /// Forbids model-visible arbitrary network access.
412 Disabled,
413 /// Permits model-visible network access.
414 Enabled,
415}
416
417/// Complete execution policy for one workspace.
418#[derive(Clone, Copy, Debug, Eq, PartialEq)]
419///
420/// ```
421/// use agents_kit::workspace::{WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
422/// let value = WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled);
423/// assert_eq!(value.sandbox_mode(), WorkspaceSandboxMode::ReadOnly);
424/// # Ok::<(), agents_kit::KitError>(())
425/// ```
426pub struct WorkspaceExecutionPolicy {
427 sandbox_mode: WorkspaceSandboxMode,
428 network_access: WorkspaceNetworkAccess,
429}
430
431impl WorkspaceExecutionPolicy {
432 /// Constructs an explicit workspace execution policy.
433 #[must_use]
434 ///
435 /// ```
436 /// use agents_kit::workspace::{WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
437 /// let value = WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled);
438 /// assert_eq!(value.sandbox_mode(), WorkspaceSandboxMode::ReadOnly);
439 /// # Ok::<(), agents_kit::KitError>(())
440 /// ```
441 pub fn new(sandbox_mode: WorkspaceSandboxMode, network_access: WorkspaceNetworkAccess) -> Self {
442 Self {
443 sandbox_mode,
444 network_access,
445 }
446 }
447
448 /// Returns the selected filesystem sandbox mode.
449 #[must_use]
450 ///
451 /// ```
452 /// use agents_kit::workspace::{WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
453 /// let value = WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled);
454 /// assert_eq!(value.sandbox_mode(), WorkspaceSandboxMode::ReadOnly);
455 /// # Ok::<(), agents_kit::KitError>(())
456 /// ```
457 pub fn sandbox_mode(&self) -> WorkspaceSandboxMode {
458 self.sandbox_mode
459 }
460
461 /// Returns the selected network access policy.
462 #[must_use]
463 ///
464 /// ```
465 /// use agents_kit::workspace::{WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
466 /// let value = WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled);
467 /// assert_eq!(value.network_access(), WorkspaceNetworkAccess::Disabled);
468 /// # Ok::<(), agents_kit::KitError>(())
469 /// ```
470 pub fn network_access(&self) -> WorkspaceNetworkAccess {
471 self.network_access
472 }
473}
474
475#[derive(Clone, Debug, Eq, PartialEq)]
476/// Declares the complete portable context for a workspace.
477///
478/// ```
479/// use agents_kit::workspace::{
480/// WorkspaceContext, WorkspaceExecutionPolicy, WorkspaceNetworkAccess,
481/// WorkspaceRoot, WorkspaceRootAccess, WorkspaceSandboxMode,
482/// };
483///
484/// let cwd = std::env::current_dir().expect("current directory");
485/// let root = WorkspaceRoot::new(cwd.clone(), WorkspaceRootAccess::ReadWrite)?;
486/// let policy = WorkspaceExecutionPolicy::new(
487/// WorkspaceSandboxMode::FullAccess,
488/// WorkspaceNetworkAccess::Enabled,
489/// );
490/// let context = WorkspaceContext::new(cwd.clone(), vec![root], policy)?;
491/// assert_eq!(context.cwd(), cwd);
492/// # Ok::<(), agents_kit::KitError>(())
493/// ```
494pub struct WorkspaceContext {
495 cwd: BackendHostPath,
496 roots: Vec<WorkspaceRoot>,
497 execution_policy: WorkspaceExecutionPolicy,
498}
499
500impl WorkspaceContext {
501 /// Validates and constructs a complete passive workspace context.
502 ///
503 /// # Errors
504 /// Returns InvalidInput for invalid absolute paths, mixed path flavors, no roots, contradictory overlapping access, a cwd outside the roots, or root access inconsistent with the sandbox policy.
505 ///
506 /// ```
507 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
508 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
509 /// let context = WorkspaceContext::new("/workspace".into(),
510 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
511 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
512 /// assert_eq!(context.roots().len(), 1);
513 /// # Ok::<(), agents_kit::KitError>(())
514 /// ```
515 pub fn new(
516 cwd: PathBuf,
517 roots: Vec<WorkspaceRoot>,
518 execution_policy: WorkspaceExecutionPolicy,
519 ) -> Result<Self, KitError> {
520 let cwd = BackendHostPath::parse_absolute("workspace cwd", cwd)?;
521 if roots.is_empty() {
522 return Err(KitError::invalid_input(
523 "workspace context requires at least one root",
524 ));
525 }
526
527 let mut deduplicated = Vec::<WorkspaceRoot>::new();
528 for root in roots {
529 if !cwd.same_flavor(&root.path) {
530 return Err(KitError::invalid_input(
531 "workspace cwd and roots must use one backend-host path flavor",
532 ));
533 }
534 if let Some(existing) = deduplicated
535 .iter()
536 .find(|existing| existing.path == root.path)
537 {
538 if existing.access != root.access {
539 return Err(KitError::invalid_input(
540 "duplicate workspace roots must not contradict access",
541 ));
542 }
543 continue;
544 }
545 if deduplicated.iter().any(|existing| {
546 existing.path.overlaps(&root.path) && existing.access != root.access
547 }) {
548 return Err(KitError::invalid_input(
549 "overlapping workspace roots must not contradict access",
550 ));
551 }
552 deduplicated.push(root);
553 }
554
555 let containing = deduplicated
556 .iter()
557 .filter(|root| root.path.contains(&cwd))
558 .collect::<Vec<_>>();
559 if containing.is_empty() {
560 return Err(KitError::invalid_input(
561 "workspace cwd must be contained by a declared root",
562 ));
563 }
564
565 match execution_policy.sandbox_mode {
566 WorkspaceSandboxMode::ReadOnly
567 if deduplicated
568 .iter()
569 .any(|root| root.access != WorkspaceRootAccess::ReadOnly) =>
570 {
571 return Err(KitError::invalid_input(
572 "read-only workspace policy requires every root to be read-only",
573 ));
574 }
575 WorkspaceSandboxMode::WorkspaceWrite
576 if containing
577 .iter()
578 .any(|root| root.access != WorkspaceRootAccess::ReadWrite) =>
579 {
580 return Err(KitError::invalid_input(
581 "workspace-write policy requires the cwd root to be read-write",
582 ));
583 }
584 WorkspaceSandboxMode::FullAccess
585 if deduplicated
586 .iter()
587 .any(|root| root.access != WorkspaceRootAccess::ReadWrite) =>
588 {
589 return Err(KitError::invalid_input(
590 "full-access workspace policy requires every root to be read-write",
591 ));
592 }
593 _ => {}
594 }
595
596 Ok(Self {
597 cwd,
598 roots: deduplicated,
599 execution_policy,
600 })
601 }
602
603 /// Returns the absolute backend-host cwd.
604 ///
605 /// ```
606 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
607 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
608 /// let context = WorkspaceContext::new("/workspace".into(),
609 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
610 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
611 /// assert_eq!(context.cwd(), std::path::Path::new("/workspace"));
612 /// # Ok::<(), agents_kit::KitError>(())
613 /// ```
614 pub fn cwd(&self) -> &Path {
615 self.cwd.as_path()
616 }
617
618 /// Returns the deduplicated declared workspace roots.
619 #[must_use]
620 ///
621 /// ```
622 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
623 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
624 /// let context = WorkspaceContext::new("/workspace".into(),
625 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
626 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
627 /// assert_eq!(context.roots()[0].access(), WorkspaceRootAccess::ReadOnly);
628 /// # Ok::<(), agents_kit::KitError>(())
629 /// ```
630 pub fn roots(&self) -> &[WorkspaceRoot] {
631 &self.roots
632 }
633
634 /// Returns the complete execution policy.
635 #[must_use]
636 ///
637 /// ```
638 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
639 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
640 /// let context = WorkspaceContext::new("/workspace".into(),
641 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
642 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
643 /// assert_eq!(context.execution_policy().network_access(), WorkspaceNetworkAccess::Disabled);
644 /// # Ok::<(), agents_kit::KitError>(())
645 /// ```
646 pub fn execution_policy(&self) -> &WorkspaceExecutionPolicy {
647 &self.execution_policy
648 }
649
650 pub(crate) fn normalized_relative(path: &Path) -> Result<PathBuf, KitError> {
651 BackendHostPath::validate_portable_relative(path)
652 }
653
654 pub(crate) fn path_flavor(&self) -> BackendHostPathFlavor {
655 self.cwd.anchor.flavor()
656 }
657
658 pub(crate) fn resolve(&self, path: &Path) -> Result<(PathBuf, PathBuf), KitError> {
659 let relative = Self::normalized_relative(path)?;
660 let absolute = self.cwd.resolve(&relative)?;
661 if !self.roots.iter().any(|root| root.path.contains(&absolute)) {
662 return Err(KitError::invalid_input(
663 "workspace path must remain beneath a declared root",
664 ));
665 }
666 Ok((relative, absolute.into_path_buf()))
667 }
668}
669
670#[derive(Clone, Debug, Eq, PartialEq)]
671/// Supplies optional model, reasoning, and knowledge defaults for a workspace.
672///
673/// ```
674/// use agents_kit::workspace::WorkspaceDefaults;
675/// let value = WorkspaceDefaults::new(None, None, None)?;
676/// assert_eq!(value.model(), None);
677/// # Ok::<(), agents_kit::KitError>(())
678/// ```
679pub struct WorkspaceDefaults {
680 model: Option<ModelSelector>,
681 personality: Option<PersonalitySelector>,
682 reasoning_effort: Option<ReasoningEffort>,
683 knowledge_policy: Option<KnowledgePolicy>,
684}
685
686impl WorkspaceDefaults {
687 /// Constructs validated workspace defaults.
688 ///
689 /// # Errors
690 /// Returns InvalidInput for an invalid explicit knowledge scope selection. Model and personality availability is checked by the receiving backend.
691 ///
692 /// ```
693 /// use agents_kit::workspace::WorkspaceDefaults;
694 /// let value = WorkspaceDefaults::new(None, None, None)?;
695 /// assert_eq!(value.model(), None);
696 /// # Ok::<(), agents_kit::KitError>(())
697 /// ```
698 pub fn new(
699 model: Option<ModelSelector>,
700 reasoning_effort: Option<ReasoningEffort>,
701 knowledge_policy: Option<KnowledgePolicy>,
702 ) -> Result<Self, KitError> {
703 if let Some(policy) = &knowledge_policy {
704 policy.validate()?;
705 }
706 Ok(Self {
707 model,
708 personality: None,
709 reasoning_effort,
710 knowledge_policy,
711 })
712 }
713
714 /// Constructs defaults with every optional value absent.
715 ///
716 /// ```
717 /// use agents_kit::workspace::WorkspaceDefaults;
718 /// let value = WorkspaceDefaults::new(None, None, None)?;
719 /// let value = WorkspaceDefaults::empty();
720 /// assert_eq!(value.knowledge_policy(), None);
721 /// # Ok::<(), agents_kit::KitError>(())
722 /// ```
723 pub fn empty() -> Self {
724 Self {
725 model: None,
726 personality: None,
727 reasoning_effort: None,
728 knowledge_policy: None,
729 }
730 }
731
732 /// Returns the optional model default.
733 ///
734 /// ```
735 /// use agents_kit::workspace::WorkspaceDefaults;
736 /// let value = WorkspaceDefaults::new(None, None, None)?;
737 /// assert_eq!(value.model(), None);
738 /// # Ok::<(), agents_kit::KitError>(())
739 /// ```
740 pub fn model(&self) -> Option<&ModelSelector> {
741 self.model.as_ref()
742 }
743
744 /// Sets the optional personality default.
745 #[must_use]
746 ///
747 /// ```
748 /// use agents_kit::workspace::WorkspaceDefaults;
749 /// let value = WorkspaceDefaults::new(None, None, None)?;
750 /// let value = value.with_personality(agents_kit::model::PersonalitySelector::new("concise")?);
751 /// assert_eq!(value.personality().unwrap().as_str(), "concise");
752 /// # Ok::<(), agents_kit::KitError>(())
753 /// ```
754 pub fn with_personality(mut self, personality: PersonalitySelector) -> Self {
755 self.personality = Some(personality);
756 self
757 }
758
759 /// Returns the optional personality default.
760 #[must_use]
761 ///
762 /// ```
763 /// use agents_kit::workspace::WorkspaceDefaults;
764 /// let value = WorkspaceDefaults::new(None, None, None)?;
765 /// assert_eq!(value.personality(), None);
766 /// # Ok::<(), agents_kit::KitError>(())
767 /// ```
768 pub fn personality(&self) -> Option<&PersonalitySelector> {
769 self.personality.as_ref()
770 }
771
772 /// Returns the optional reasoning-effort default.
773 ///
774 /// ```
775 /// use agents_kit::workspace::WorkspaceDefaults;
776 /// let value = WorkspaceDefaults::new(None, None, None)?;
777 /// assert_eq!(value.reasoning_effort(), None);
778 /// # Ok::<(), agents_kit::KitError>(())
779 /// ```
780 pub fn reasoning_effort(&self) -> Option<&ReasoningEffort> {
781 self.reasoning_effort.as_ref()
782 }
783
784 /// Returns the optional managed-knowledge policy default.
785 ///
786 /// ```
787 /// use agents_kit::workspace::WorkspaceDefaults;
788 /// let value = WorkspaceDefaults::new(None, None, None)?;
789 /// assert_eq!(value.knowledge_policy(), None);
790 /// # Ok::<(), agents_kit::KitError>(())
791 /// ```
792 pub fn knowledge_policy(&self) -> Option<&KnowledgePolicy> {
793 self.knowledge_policy.as_ref()
794 }
795}
796
797#[derive(Clone, Debug, Eq, PartialEq)]
798/// Describes one workspace to create.
799///
800/// ```
801/// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
802/// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
803/// let context = WorkspaceContext::new("/workspace".into(),
804/// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
805/// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
806/// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
807/// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
808/// assert_eq!(value.name(), "review");
809/// # Ok::<(), agents_kit::KitError>(())
810/// ```
811pub struct NewWorkspace {
812 name: String,
813 context: WorkspaceContext,
814 defaults: WorkspaceDefaults,
815}
816
817impl NewWorkspace {
818 /// Validates and constructs a new-workspace request.
819 ///
820 /// # Errors
821 /// Returns InvalidInput for an empty or padded name.
822 ///
823 /// ```
824 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
825 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
826 /// let context = WorkspaceContext::new("/workspace".into(),
827 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
828 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
829 /// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
830 /// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
831 /// assert_eq!(value.name(), "review");
832 /// # Ok::<(), agents_kit::KitError>(())
833 /// ```
834 pub fn new(
835 name: impl Into<String>,
836 context: WorkspaceContext,
837 defaults: WorkspaceDefaults,
838 ) -> Result<Self, KitError> {
839 let name = name.into();
840 Self::validate_name(&name)?;
841 Ok(Self {
842 name,
843 context,
844 defaults,
845 })
846 }
847
848 /// Returns the workspace name.
849 ///
850 /// ```
851 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
852 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
853 /// let context = WorkspaceContext::new("/workspace".into(),
854 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
855 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
856 /// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
857 /// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
858 /// assert_eq!(value.name(), "review");
859 /// # Ok::<(), agents_kit::KitError>(())
860 /// ```
861 pub fn name(&self) -> &str {
862 &self.name
863 }
864
865 /// Returns the complete workspace context.
866 ///
867 /// ```
868 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
869 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
870 /// let context = WorkspaceContext::new("/workspace".into(),
871 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
872 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
873 /// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
874 /// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
875 /// assert_eq!(value.context().cwd(), std::path::Path::new("/workspace"));
876 /// # Ok::<(), agents_kit::KitError>(())
877 /// ```
878 pub fn context(&self) -> &WorkspaceContext {
879 &self.context
880 }
881
882 /// Returns the complete workspace defaults.
883 ///
884 /// ```
885 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
886 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
887 /// let context = WorkspaceContext::new("/workspace".into(),
888 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
889 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
890 /// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
891 /// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
892 /// assert_eq!(value.defaults().model(), None);
893 /// # Ok::<(), agents_kit::KitError>(())
894 /// ```
895 pub fn defaults(&self) -> &WorkspaceDefaults {
896 &self.defaults
897 }
898
899 fn validate_name(name: &str) -> Result<(), KitError> {
900 if name.is_empty() || name.trim() != name {
901 return Err(KitError::invalid_input(
902 "workspace name must be nonempty and unpadded",
903 ));
904 }
905 Ok(())
906 }
907}
908
909#[derive(Clone, Debug, Eq, PartialEq)]
910/// Contains one committed workspace snapshot.
911///
912/// ```
913/// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
914/// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
915/// let context = WorkspaceContext::new("/workspace".into(),
916/// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
917/// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
918/// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
919/// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
920/// assert_eq!(value.id().as_str(), "workspace_00000000000000000000000000000000");
921/// # Ok::<(), agents_kit::KitError>(())
922/// ```
923pub struct WorkspaceInfo {
924 id: WorkspaceId,
925 name: String,
926 context: WorkspaceContext,
927 defaults: WorkspaceDefaults,
928 revision: u64,
929}
930
931impl WorkspaceInfo {
932 /// Validates and constructs one committed workspace snapshot.
933 ///
934 /// The name must be nonempty and unpadded, and `revision` must be
935 /// nonzero.
936 ///
937 /// # Errors
938 ///
939 /// Returns [`crate::error::ErrorKind::InvalidInput`] for an invalid name or
940 /// [`crate::error::ErrorKind::InvalidState`] for a zero revision.
941 ///
942 /// # Examples
943 ///
944 /// ```
945 /// use agents_kit::{
946 /// identity::WorkspaceId,
947 /// workspace::{
948 /// WorkspaceContext, WorkspaceDefaults, WorkspaceExecutionPolicy,
949 /// WorkspaceInfo, WorkspaceNetworkAccess, WorkspaceRoot,
950 /// WorkspaceRootAccess, WorkspaceSandboxMode,
951 /// },
952 /// };
953 ///
954 /// let cwd = std::env::current_dir()?;
955 /// let context = WorkspaceContext::new(
956 /// cwd.clone(),
957 /// vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadWrite)?],
958 /// WorkspaceExecutionPolicy::new(
959 /// WorkspaceSandboxMode::FullAccess,
960 /// WorkspaceNetworkAccess::Enabled,
961 /// ),
962 /// )?;
963 /// let id: WorkspaceId =
964 /// "workspace_00000000000000000000000000000000".parse()?;
965 /// let info = WorkspaceInfo::new(
966 /// id,
967 /// "release".to_owned(),
968 /// context,
969 /// WorkspaceDefaults::empty(),
970 /// 1,
971 /// )?;
972 /// assert_eq!(info.revision(), 1);
973 /// # Ok::<(), Box<dyn std::error::Error>>(())
974 /// ```
975 pub fn new(
976 id: WorkspaceId,
977 name: String,
978 context: WorkspaceContext,
979 defaults: WorkspaceDefaults,
980 revision: u64,
981 ) -> Result<Self, KitError> {
982 NewWorkspace::validate_name(&name)?;
983 if revision == 0 {
984 return Err(KitError::invalid_state(
985 "workspace revision must be nonzero",
986 ));
987 }
988 Ok(Self {
989 id,
990 name,
991 context,
992 defaults,
993 revision,
994 })
995 }
996
997 /// Returns the portable workspace identity.
998 ///
999 /// ```
1000 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1001 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1002 /// let context = WorkspaceContext::new("/workspace".into(),
1003 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1004 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1005 /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
1006 /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
1007 /// assert_eq!(value.id().as_str(), "workspace_00000000000000000000000000000000");
1008 /// # Ok::<(), agents_kit::KitError>(())
1009 /// ```
1010 pub fn id(&self) -> &WorkspaceId {
1011 &self.id
1012 }
1013
1014 /// Returns the committed workspace name.
1015 ///
1016 /// ```
1017 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1018 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1019 /// let context = WorkspaceContext::new("/workspace".into(),
1020 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1021 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1022 /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
1023 /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
1024 /// assert_eq!(value.name(), "review");
1025 /// # Ok::<(), agents_kit::KitError>(())
1026 /// ```
1027 pub fn name(&self) -> &str {
1028 &self.name
1029 }
1030
1031 /// Returns the committed workspace context.
1032 ///
1033 /// ```
1034 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1035 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1036 /// let context = WorkspaceContext::new("/workspace".into(),
1037 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1038 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1039 /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
1040 /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
1041 /// assert_eq!(value.context().cwd(), std::path::Path::new("/workspace"));
1042 /// # Ok::<(), agents_kit::KitError>(())
1043 /// ```
1044 pub fn context(&self) -> &WorkspaceContext {
1045 &self.context
1046 }
1047
1048 /// Returns the committed workspace defaults.
1049 ///
1050 /// ```
1051 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1052 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1053 /// let context = WorkspaceContext::new("/workspace".into(),
1054 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1055 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1056 /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
1057 /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
1058 /// assert_eq!(value.defaults().model(), None);
1059 /// # Ok::<(), agents_kit::KitError>(())
1060 /// ```
1061 pub fn defaults(&self) -> &WorkspaceDefaults {
1062 &self.defaults
1063 }
1064
1065 /// Returns the nonzero committed revision.
1066 ///
1067 /// ```
1068 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1069 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1070 /// let context = WorkspaceContext::new("/workspace".into(),
1071 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1072 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1073 /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
1074 /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
1075 /// assert_eq!(value.revision(), 1);
1076 /// # Ok::<(), agents_kit::KitError>(())
1077 /// ```
1078 pub fn revision(&self) -> u64 {
1079 self.revision
1080 }
1081}
1082
1083#[derive(Clone, Debug, Eq, PartialEq)]
1084/// Proposes a revision-checked complete workspace replacement.
1085///
1086/// ```
1087/// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1088/// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1089/// let context = WorkspaceContext::new("/workspace".into(),
1090/// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1091/// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1092/// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1093/// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1094/// assert_eq!(value.expected_revision(), 1);
1095/// # Ok::<(), agents_kit::KitError>(())
1096/// ```
1097pub struct UpdateWorkspaceRequest {
1098 id: WorkspaceId,
1099 expected_revision: u64,
1100 name: String,
1101 context: WorkspaceContext,
1102 defaults: WorkspaceDefaults,
1103}
1104
1105impl UpdateWorkspaceRequest {
1106 /// Validates and constructs a workspace update request.
1107 ///
1108 /// # Errors
1109 /// Returns InvalidInput for an empty/padded name or zero expected revision. Constructing a request does not reserve or change that revision.
1110 ///
1111 /// ```
1112 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1113 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1114 /// let context = WorkspaceContext::new("/workspace".into(),
1115 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1116 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1117 /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1118 /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1119 /// assert_eq!(value.expected_revision(), 1);
1120 /// # Ok::<(), agents_kit::KitError>(())
1121 /// ```
1122 pub fn new(
1123 id: WorkspaceId,
1124 expected_revision: u64,
1125 name: impl Into<String>,
1126 context: WorkspaceContext,
1127 defaults: WorkspaceDefaults,
1128 ) -> Result<Self, KitError> {
1129 let name = name.into();
1130 NewWorkspace::validate_name(&name)?;
1131 if expected_revision == 0 {
1132 return Err(KitError::invalid_input(
1133 "expected workspace revision must be nonzero",
1134 ));
1135 }
1136 Ok(Self {
1137 id,
1138 expected_revision,
1139 name,
1140 context,
1141 defaults,
1142 })
1143 }
1144
1145 /// Returns the workspace identity to update.
1146 ///
1147 /// ```
1148 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1149 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1150 /// let context = WorkspaceContext::new("/workspace".into(),
1151 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1152 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1153 /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1154 /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1155 /// assert_eq!(value.id().as_str(), "workspace_00000000000000000000000000000000");
1156 /// # Ok::<(), agents_kit::KitError>(())
1157 /// ```
1158 pub fn id(&self) -> &WorkspaceId {
1159 &self.id
1160 }
1161
1162 /// Returns the revision that must still be current.
1163 ///
1164 /// ```
1165 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1166 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1167 /// let context = WorkspaceContext::new("/workspace".into(),
1168 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1169 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1170 /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1171 /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1172 /// assert_eq!(value.expected_revision(), 1);
1173 /// # Ok::<(), agents_kit::KitError>(())
1174 /// ```
1175 pub fn expected_revision(&self) -> u64 {
1176 self.expected_revision
1177 }
1178
1179 /// Returns the proposed replacement name.
1180 ///
1181 /// ```
1182 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1183 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1184 /// let context = WorkspaceContext::new("/workspace".into(),
1185 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1186 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1187 /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1188 /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1189 /// assert_eq!(value.name(), "review");
1190 /// # Ok::<(), agents_kit::KitError>(())
1191 /// ```
1192 pub fn name(&self) -> &str {
1193 &self.name
1194 }
1195
1196 /// Returns the proposed replacement context.
1197 ///
1198 /// ```
1199 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1200 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1201 /// let context = WorkspaceContext::new("/workspace".into(),
1202 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1203 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1204 /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1205 /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1206 /// assert_eq!(value.context().cwd(), std::path::Path::new("/workspace"));
1207 /// # Ok::<(), agents_kit::KitError>(())
1208 /// ```
1209 pub fn context(&self) -> &WorkspaceContext {
1210 &self.context
1211 }
1212
1213 /// Returns the proposed replacement defaults.
1214 ///
1215 /// ```
1216 /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1217 /// WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1218 /// let context = WorkspaceContext::new("/workspace".into(),
1219 /// vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1220 /// WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1221 /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1222 /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1223 /// assert_eq!(value.defaults().model(), None);
1224 /// # Ok::<(), agents_kit::KitError>(())
1225 /// ```
1226 pub fn defaults(&self) -> &WorkspaceDefaults {
1227 &self.defaults
1228 }
1229}
1230
1231#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1232/// Reports advisory readiness for a workspace update.
1233pub enum WorkspaceUpdateReadiness {
1234 /// The current snapshot has no known blocker.
1235 Ready,
1236 /// Current admitted or descendant work blocks mutation.
1237 Busy,
1238}
1239
1240#[derive(Clone, Debug, Eq, PartialEq)]
1241/// Requests revision-checked deletion of one quiescent workspace.
1242///
1243/// ```
1244/// use agents_kit::workspace::DeleteWorkspaceRequest;
1245/// let value = DeleteWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1)?;
1246/// assert_eq!(value.expected_revision(), 1);
1247/// # Ok::<(), agents_kit::KitError>(())
1248/// ```
1249pub struct DeleteWorkspaceRequest {
1250 id: WorkspaceId,
1251 expected_revision: u64,
1252}
1253
1254impl DeleteWorkspaceRequest {
1255 /// Validates and constructs a workspace deletion request.
1256 ///
1257 /// # Errors
1258 /// Returns InvalidInput for a zero expected revision. Actual deletion also requires current revision and quiescence.
1259 ///
1260 /// ```
1261 /// use agents_kit::workspace::DeleteWorkspaceRequest;
1262 /// let value = DeleteWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1)?;
1263 /// assert_eq!(value.expected_revision(), 1);
1264 /// # Ok::<(), agents_kit::KitError>(())
1265 /// ```
1266 pub fn new(id: WorkspaceId, expected_revision: u64) -> Result<Self, KitError> {
1267 if expected_revision == 0 {
1268 return Err(KitError::invalid_input(
1269 "expected workspace revision must be nonzero",
1270 ));
1271 }
1272 Ok(Self {
1273 id,
1274 expected_revision,
1275 })
1276 }
1277
1278 /// Returns the workspace identity to delete.
1279 ///
1280 /// ```
1281 /// use agents_kit::workspace::DeleteWorkspaceRequest;
1282 /// let value = DeleteWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1)?;
1283 /// assert_eq!(value.id().as_str(), "workspace_00000000000000000000000000000000");
1284 /// # Ok::<(), agents_kit::KitError>(())
1285 /// ```
1286 pub fn id(&self) -> &WorkspaceId {
1287 &self.id
1288 }
1289
1290 /// Returns the revision that must still be current.
1291 ///
1292 /// ```
1293 /// use agents_kit::workspace::DeleteWorkspaceRequest;
1294 /// let value = DeleteWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1)?;
1295 /// assert_eq!(value.expected_revision(), 1);
1296 /// # Ok::<(), agents_kit::KitError>(())
1297 /// ```
1298 pub fn expected_revision(&self) -> u64 {
1299 self.expected_revision
1300 }
1301}
1302
1303/// Public implementation contract for one live portable workspace.
1304///
1305/// A caller agent returns its own concrete workspace implementation through
1306/// [`AgentBackend::Workspace`](crate::AgentBackend::Workspace), and this workspace
1307/// returns its own concrete [`Conversation`](Self::Conversation) implementation.
1308/// Runtime composition can instead use a caller-owned exhaustive enum hierarchy.
1309/// Closing consumes the implementation, seals new work, and invalidates live
1310/// conversations after accepted cleanup settles. Drop alone carries no
1311/// asynchronous cleanup guarantee.
1312///
1313/// # Examples
1314///
1315/// This local Custom agent uses in-memory Lance and sends no network requests.
1316///
1317/// ```
1318/// # use std::{num::NonZeroUsize, time::Duration};
1319/// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1320/// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1321/// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1322/// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1323/// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1324/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1325/// # runtime.block_on(async {
1326/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1327/// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1328/// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1329/// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1330/// # let provider = Provider::from_config(ProviderConfig::new(
1331/// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1332/// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1333/// # let outcome = async {
1334/// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1335/// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1336/// # let cwd = std::env::current_dir()?;
1337/// # let context = WorkspaceContext::new(cwd.clone(),
1338/// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1339/// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1340/// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1341/// assert!(workspace.list_conversations().await?.is_empty());
1342/// assert_eq!(workspace.info().await?.id(), &workspace.id());
1343/// workspace.close().await?;
1344/// # Ok::<(), Box<dyn std::error::Error>>(())
1345/// # }.await;
1346/// # let cleanup = agent.shutdown().await;
1347/// # outcome?;
1348/// # cleanup?;
1349/// # Ok::<(), Box<dyn std::error::Error>>(())
1350/// # })?;
1351/// # Ok::<(), Box<dyn std::error::Error>>(())
1352/// ```
1353pub trait WorkspaceBackend: Send + Sync + 'static {
1354 /// Concrete conversation implementation produced by this workspace.
1355 type Conversation: crate::ConversationBackend;
1356
1357 /// Returns the durable portable workspace identity.
1358 ///
1359 /// This is the durable workspace identity. A handle also carries a runtime
1360 /// revision binding, so a stable id does not make an old handle current.
1361 ///
1362 /// # Examples
1363 ///
1364 /// This local Custom agent uses in-memory Lance and sends no network requests.
1365 ///
1366 /// ```
1367 /// # use std::{num::NonZeroUsize, time::Duration};
1368 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1369 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1370 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1371 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1372 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1373 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1374 /// # runtime.block_on(async {
1375 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1376 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1377 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1378 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1379 /// # let provider = Provider::from_config(ProviderConfig::new(
1380 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1381 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1382 /// # let outcome = async {
1383 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1384 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1385 /// # let cwd = std::env::current_dir()?;
1386 /// # let context = WorkspaceContext::new(cwd.clone(),
1387 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1388 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1389 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1390 /// let id = workspace.id();
1391 /// assert_eq!(workspace.info().await?.id(), &id);
1392 /// # workspace.close().await?;
1393 /// # Ok::<(), Box<dyn std::error::Error>>(())
1394 /// # }.await;
1395 /// # let cleanup = agent.shutdown().await;
1396 /// # outcome?;
1397 /// # cleanup?;
1398 /// # Ok::<(), Box<dyn std::error::Error>>(())
1399 /// # })?;
1400 /// # Ok::<(), Box<dyn std::error::Error>>(())
1401 /// ```
1402 fn id(&self) -> WorkspaceId;
1403
1404 /// Reads the current committed workspace information.
1405 ///
1406 /// The result contains the complete committed context/defaults and revision.
1407 /// It does not refresh this handle after a concurrent revision change.
1408 ///
1409 /// # Errors
1410 ///
1411 /// Returns [`KitError`] for a closed parent/workspace, stale revision,
1412 /// state-read failure, inconsistent durable state, or deadline expiry.
1413 ///
1414 /// # Examples
1415 ///
1416 /// This local Custom agent uses in-memory Lance and sends no network requests.
1417 ///
1418 /// ```
1419 /// # use std::{num::NonZeroUsize, time::Duration};
1420 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1421 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1422 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1423 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1424 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1425 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1426 /// # runtime.block_on(async {
1427 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1428 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1429 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1430 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1431 /// # let provider = Provider::from_config(ProviderConfig::new(
1432 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1433 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1434 /// # let outcome = async {
1435 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1436 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1437 /// # let cwd = std::env::current_dir()?;
1438 /// # let context = WorkspaceContext::new(cwd.clone(),
1439 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1440 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1441 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1442 /// let info = workspace.info().await?;
1443 /// assert_eq!(info.name(), "review");
1444 /// assert_eq!(info.revision(), 1);
1445 /// # workspace.close().await?;
1446 /// # Ok::<(), Box<dyn std::error::Error>>(())
1447 /// # }.await;
1448 /// # let cleanup = agent.shutdown().await;
1449 /// # outcome?;
1450 /// # cleanup?;
1451 /// # Ok::<(), Box<dyn std::error::Error>>(())
1452 /// # })?;
1453 /// # Ok::<(), Box<dyn std::error::Error>>(())
1454 /// ```
1455 fn info(&self) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
1456
1457 /// Creates and publishes one conversation under this workspace.
1458 ///
1459 /// The returned associated child is published only after resolved defaults,
1460 /// initial knowledge, backend attachment, and durable ownership agree.
1461 ///
1462 /// # Errors
1463 ///
1464 /// Returns [`KitError`] for closed or stale runtime, busy admission, invalid
1465 /// input or unsupported policy/integrations, and dependency/deadline failures.
1466 /// Accepted native setup remains durably attached or recovery-owned on failure.
1467 ///
1468 /// # Examples
1469 ///
1470 /// This local Custom agent uses in-memory Lance and sends no network requests.
1471 ///
1472 /// ```
1473 /// # use std::{num::NonZeroUsize, time::Duration};
1474 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1475 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1476 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1477 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1478 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1479 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1480 /// # runtime.block_on(async {
1481 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1482 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1483 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1484 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1485 /// # let provider = Provider::from_config(ProviderConfig::new(
1486 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1487 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1488 /// # let outcome = async {
1489 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1490 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1491 /// # let cwd = std::env::current_dir()?;
1492 /// # let context = WorkspaceContext::new(cwd.clone(),
1493 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1494 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1495 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1496 /// use agents_kit::ConversationBackend;
1497 /// use agents_kit::conversation::NewConversation;
1498 /// let conversation = workspace.new_conversation(NewConversation::new(
1499 /// Some("Review".into()), None, Vec::new(),
1500 /// )?).await?;
1501 /// assert_eq!(conversation.info().await?.title(), Some("Review"));
1502 /// // Parent close preserves this quiescent durable conversation for reopen.
1503 /// workspace.close().await?;
1504 /// # Ok::<(), Box<dyn std::error::Error>>(())
1505 /// # }.await;
1506 /// # let cleanup = agent.shutdown().await;
1507 /// # outcome?;
1508 /// # cleanup?;
1509 /// # Ok::<(), Box<dyn std::error::Error>>(())
1510 /// # })?;
1511 /// # Ok::<(), Box<dyn std::error::Error>>(())
1512 /// ```
1513 fn new_conversation(
1514 &self,
1515 request: NewConversation,
1516 ) -> impl Future<Output = Result<Self::Conversation, KitError>> + Send + '_;
1517
1518 /// Lists live or reopenable conversations in deterministic identity order.
1519 ///
1520 /// Inventory comes from ownership-valid durable records; it is not limited
1521 /// to handles currently open in this process. Terminally closed conversations
1522 /// are excluded. Listing does not claim live handles.
1523 ///
1524 /// # Errors
1525 ///
1526 /// Returns [`KitError`] for closed or stale runtime, state/dependency read
1527 /// failures, inconsistent inventory, or deadline expiry.
1528 ///
1529 /// # Examples
1530 ///
1531 /// This local Custom agent uses in-memory Lance and sends no network requests.
1532 ///
1533 /// ```
1534 /// # use std::{num::NonZeroUsize, time::Duration};
1535 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1536 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1537 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1538 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1539 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1540 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1541 /// # runtime.block_on(async {
1542 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1543 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1544 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1545 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1546 /// # let provider = Provider::from_config(ProviderConfig::new(
1547 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1548 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1549 /// # let outcome = async {
1550 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1551 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1552 /// # let cwd = std::env::current_dir()?;
1553 /// # let context = WorkspaceContext::new(cwd.clone(),
1554 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1555 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1556 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1557 /// assert!(workspace.list_conversations().await?.is_empty());
1558 /// # workspace.close().await?;
1559 /// # Ok::<(), Box<dyn std::error::Error>>(())
1560 /// # }.await;
1561 /// # let cleanup = agent.shutdown().await;
1562 /// # outcome?;
1563 /// # cleanup?;
1564 /// # Ok::<(), Box<dyn std::error::Error>>(())
1565 /// # })?;
1566 /// # Ok::<(), Box<dyn std::error::Error>>(())
1567 /// ```
1568 fn list_conversations(
1569 &self,
1570 ) -> impl Future<Output = Result<Vec<ConversationInfo>, KitError>> + Send + '_;
1571
1572 /// Reads committed information for one conversation identity.
1573 ///
1574 /// This reads committed metadata without claiming a live child handle.
1575 /// Missing and foreign identities do not reveal a foreign owner.
1576 ///
1577 /// # Errors
1578 ///
1579 /// Returns [`crate::error::ErrorKind::NotFound`] for an unavailable or foreign
1580 /// conversation, or the owning closed/stale runtime, state-read, integrity, or
1581 /// deadline error.
1582 ///
1583 /// # Examples
1584 ///
1585 /// This local Custom agent uses in-memory Lance and sends no network requests.
1586 ///
1587 /// ```
1588 /// # use std::{num::NonZeroUsize, time::Duration};
1589 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1590 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1591 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1592 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1593 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1594 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1595 /// # runtime.block_on(async {
1596 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1597 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1598 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1599 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1600 /// # let provider = Provider::from_config(ProviderConfig::new(
1601 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1602 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1603 /// # let outcome = async {
1604 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1605 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1606 /// # let cwd = std::env::current_dir()?;
1607 /// # let context = WorkspaceContext::new(cwd.clone(),
1608 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1609 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1610 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1611 /// use agents_kit::ConversationBackend;
1612 /// use agents_kit::conversation::NewConversation;
1613 /// let conversation = workspace.new_conversation(NewConversation::new(
1614 /// Some("Review".into()), None, Vec::new(),
1615 /// )?).await?;
1616 /// let info = workspace.conversation_info(conversation.id()).await?;
1617 /// assert_eq!(info.id(), &conversation.id());
1618 /// assert_eq!(info.title(), Some("Review"));
1619 /// # workspace.close().await?;
1620 /// # Ok::<(), Box<dyn std::error::Error>>(())
1621 /// # }.await;
1622 /// # let cleanup = agent.shutdown().await;
1623 /// # outcome?;
1624 /// # cleanup?;
1625 /// # Ok::<(), Box<dyn std::error::Error>>(())
1626 /// # })?;
1627 /// # Ok::<(), Box<dyn std::error::Error>>(())
1628 /// ```
1629 fn conversation_info(
1630 &self,
1631 id: ConversationId,
1632 ) -> impl Future<Output = Result<ConversationInfo, KitError>> + Send + '_;
1633
1634 /// Opens the current live handle for one conversation identity.
1635 ///
1636 /// A current conversation has at most one live public handle; repeated lookup
1637 /// is busy until its claim is released. Reopen verifies the current workspace
1638 /// revision, defaults, attachments, tools, knowledge, and recovery state. It
1639 /// does not create a replacement session for a missing identity.
1640 ///
1641 /// # Errors
1642 ///
1643 /// Returns [`KitError`] for a missing/foreign or terminal conversation, an
1644 /// existing live claim, closed or stale runtime, incompatible reopen state,
1645 /// or dependency/deadline failure.
1646 ///
1647 /// # Examples
1648 ///
1649 /// This local Custom agent uses in-memory Lance and sends no network requests.
1650 ///
1651 /// ```
1652 /// # use std::{num::NonZeroUsize, time::Duration};
1653 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1654 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1655 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1656 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1657 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1658 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1659 /// # runtime.block_on(async {
1660 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1661 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1662 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1663 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1664 /// # let provider = Provider::from_config(ProviderConfig::new(
1665 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1666 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1667 /// # let outcome = async {
1668 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1669 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1670 /// # let cwd = std::env::current_dir()?;
1671 /// # let context = WorkspaceContext::new(cwd.clone(),
1672 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1673 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1674 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1675 /// use agents_kit::ConversationBackend;
1676 /// use agents_kit::conversation::NewConversation;
1677 /// let conversation = workspace.new_conversation(NewConversation::new(None, None, Vec::new())?).await?;
1678 /// let id = conversation.id();
1679 /// let workspace_id = workspace.id();
1680 /// workspace.close().await?;
1681 /// let workspace = agent.get_workspace(workspace_id).await?;
1682 /// let reopened = workspace.get_conversation(id.clone()).await?;
1683 /// assert_eq!(reopened.id(), id);
1684 /// workspace.close().await?;
1685 /// # Ok::<(), Box<dyn std::error::Error>>(())
1686 /// # }.await;
1687 /// # let cleanup = agent.shutdown().await;
1688 /// # outcome?;
1689 /// # cleanup?;
1690 /// # Ok::<(), Box<dyn std::error::Error>>(())
1691 /// # })?;
1692 /// # Ok::<(), Box<dyn std::error::Error>>(())
1693 /// ```
1694 fn get_conversation(
1695 &self,
1696 id: ConversationId,
1697 ) -> impl Future<Output = Result<Self::Conversation, KitError>> + Send + '_;
1698
1699 /// Lists agents-kit-managed tools installed for this workspace.
1700 ///
1701 /// Returns the complete source-neutral catalog the Agent owns and can expose
1702 /// in this workspace, subject to its execution policy. An unavailable source
1703 /// must not be silently omitted from a purportedly complete result.
1704 ///
1705 /// # Errors
1706 ///
1707 /// Returns [`KitError`] for closed or stale runtime, unsupported or incomplete
1708 /// catalog exposure, malformed source data, or dependency/deadline failure.
1709 ///
1710 /// # Examples
1711 ///
1712 /// This local Custom agent uses in-memory Lance and sends no network requests.
1713 ///
1714 /// ```
1715 /// # use std::{num::NonZeroUsize, time::Duration};
1716 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1717 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1718 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1719 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1720 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1721 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1722 /// # runtime.block_on(async {
1723 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1724 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1725 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1726 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1727 /// # let provider = Provider::from_config(ProviderConfig::new(
1728 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1729 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1730 /// # let outcome = async {
1731 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1732 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1733 /// # let cwd = std::env::current_dir()?;
1734 /// # let context = WorkspaceContext::new(cwd.clone(),
1735 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1736 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1737 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1738 /// // This Agent was constructed without Toolsets.
1739 /// assert!(workspace.list_tools().await?.is_empty());
1740 /// # workspace.close().await?;
1741 /// # Ok::<(), Box<dyn std::error::Error>>(())
1742 /// # }.await;
1743 /// # let cleanup = agent.shutdown().await;
1744 /// # outcome?;
1745 /// # cleanup?;
1746 /// # Ok::<(), Box<dyn std::error::Error>>(())
1747 /// # })?;
1748 /// # Ok::<(), Box<dyn std::error::Error>>(())
1749 /// ```
1750 fn list_tools(&self) -> impl Future<Output = Result<Vec<ToolInfo>, KitError>> + Send + '_;
1751
1752 /// Atomically replaces managed knowledge from this workspace.
1753 ///
1754 /// An omitted scope selects this Workspace. Explicit Agent or Database scope
1755 /// is validated under the shared knowledge rules; Conversation scope is not
1756 /// accepted here. Empty sources are a no-op after scope validation, not deletion.
1757 /// Updating workspace context/defaults does not move sources. See [`crate::knowledge`]
1758 /// for local ingestion and policy examples.
1759 ///
1760 /// # Errors
1761 ///
1762 /// Returns [`KitError`] for invalid sources or scope, missing/incompatible
1763 /// knowledge integrations, closed or stale runtime, busy admission, or
1764 /// dependency/deadline failure. Unsupported semantics fail explicitly.
1765 ///
1766 /// # Examples
1767 ///
1768 /// This example requires a live Workspace under an open Agent, with configured
1769 /// backend and storage integrations. It is compiled without starting services.
1770 ///
1771 /// ```no_run
1772 /// use agents_kit::{WorkspaceBackend, KitError};
1773 /// use agents_kit::knowledge::IngestKnowledgeRequest;
1774 ///
1775 /// async fn replace_sources<W: WorkspaceBackend>(workspace: &W, request: IngestKnowledgeRequest)
1776 /// -> Result<(), KitError>
1777 /// {
1778 /// workspace.ingest_knowledge(request).await
1779 /// }
1780 /// ```
1781 fn ingest_knowledge(
1782 &self,
1783 request: IngestKnowledgeRequest,
1784 ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
1785
1786 /// Consumes the implementation, seals its runtime, and settles accepted work.
1787 ///
1788 /// Close cascade-invalidates live descendants after accepted work and
1789 /// interactions settle. Quiescent durable conversations, notifications, and
1790 /// knowledge remain for reopen. This does not terminally close conversations
1791 /// or acknowledge caller events. Drop alone provides no async cleanup guarantee.
1792 ///
1793 /// # Errors
1794 ///
1795 /// Returns [`KitError`] when an owned cleanup responsibility fails or cannot
1796 /// settle within the lifecycle deadline. The consumed handle is not restored;
1797 /// durable recovery owns later reconciliation and supplementary failures do
1798 /// not replace the primary error.
1799 ///
1800 /// # Examples
1801 ///
1802 /// This local Custom agent uses in-memory Lance and sends no network requests.
1803 ///
1804 /// ```
1805 /// # use std::{num::NonZeroUsize, time::Duration};
1806 /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1807 /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1808 /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1809 /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1810 /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1811 /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1812 /// # runtime.block_on(async {
1813 /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1814 /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1815 /// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1816 /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1817 /// # let provider = Provider::from_config(ProviderConfig::new(
1818 /// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1819 /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1820 /// # let outcome = async {
1821 /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1822 /// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1823 /// # let cwd = std::env::current_dir()?;
1824 /// # let context = WorkspaceContext::new(cwd.clone(),
1825 /// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1826 /// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1827 /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1828 /// let id = workspace.id();
1829 /// workspace.close().await?;
1830 /// // Runtime closure does not delete the durable workspace.
1831 /// assert_eq!(agent.workspace_info(id).await?.name(), "review");
1832 /// # Ok::<(), Box<dyn std::error::Error>>(())
1833 /// # }.await;
1834 /// # let cleanup = agent.shutdown().await;
1835 /// # outcome?;
1836 /// # cleanup?;
1837 /// # Ok::<(), Box<dyn std::error::Error>>(())
1838 /// # })?;
1839 /// # Ok::<(), Box<dyn std::error::Error>>(())
1840 /// ```
1841 fn close(self) -> impl Future<Output = Result<(), KitError>> + Send
1842 where
1843 Self: Sized;
1844}
1845
1846/// Owns one live workspace runtime and its conversation handles.
1847///
1848/// Obtain this unique, non-Clone handle from [`crate::AgentBackend`]. Import
1849/// [`WorkspaceBackend`] for behavior. A committed workspace update invalidates
1850/// prior-revision handles; reopen by typed id instead of expecting in-place
1851/// refresh. Explicit close settles the runtime while preserving quiescent
1852/// durable children for later reopen.
1853///
1854/// # Examples
1855///
1856/// This local Custom agent uses in-memory Lance and sends no network requests.
1857///
1858/// ```
1859/// # use std::{num::NonZeroUsize, time::Duration};
1860/// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1861/// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1862/// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1863/// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1864/// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1865/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1866/// # runtime.block_on(async {
1867/// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1868/// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1869/// # CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1870/// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1871/// # let provider = Provider::from_config(ProviderConfig::new(
1872/// # ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1873/// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1874/// # let outcome = async {
1875/// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1876/// # WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1877/// # let cwd = std::env::current_dir()?;
1878/// # let context = WorkspaceContext::new(cwd.clone(),
1879/// # vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1880/// # WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1881/// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1882/// let id = workspace.id();
1883/// assert_eq!(workspace.info().await?.revision(), 1);
1884/// workspace.close().await?;
1885/// let reopened = agent.get_workspace(id.clone()).await?;
1886/// assert_eq!(reopened.id(), id);
1887/// reopened.close().await?;
1888/// # Ok::<(), Box<dyn std::error::Error>>(())
1889/// # }.await;
1890/// # let cleanup = agent.shutdown().await;
1891/// # outcome?;
1892/// # cleanup?;
1893/// # Ok::<(), Box<dyn std::error::Error>>(())
1894/// # })?;
1895/// # Ok::<(), Box<dyn std::error::Error>>(())
1896/// ```
1897pub struct Workspace<
1898 S: StateStoreBackend = storage_kit::StateStore,
1899 D: DatabaseBackend = storage_kit::Database,
1900 P: ProviderBackend = providers_kit::Provider,
1901 T: ToolsetBackend = tools_kit::Toolset,
1902> {
1903 inner: WorkspaceInner<S, D, P, T>,
1904}
1905
1906enum WorkspaceInner<S: StateStoreBackend, D: DatabaseBackend, P: ProviderBackend, T: ToolsetBackend>
1907{
1908 Codex(codex::workspace::CodexWorkspace<S, D, T>),
1909 OpenCode(opencode::workspace::OpenCodeWorkspace<S, D>),
1910 Custom(custom::CustomWorkspace<S, D, P, T>),
1911}
1912
1913impl<S, D, P, T> Workspace<S, D, P, T>
1914where
1915 S: StateStoreBackend,
1916 D: DatabaseBackend,
1917 P: ProviderBackend,
1918 T: ToolsetBackend,
1919{
1920 pub(crate) fn codex(backend: codex::workspace::CodexWorkspace<S, D, T>) -> Self {
1921 Self {
1922 inner: WorkspaceInner::Codex(backend),
1923 }
1924 }
1925
1926 pub(crate) fn opencode(backend: opencode::workspace::OpenCodeWorkspace<S, D>) -> Self {
1927 Self {
1928 inner: WorkspaceInner::OpenCode(backend),
1929 }
1930 }
1931
1932 pub(crate) fn custom(backend: custom::CustomWorkspace<S, D, P, T>) -> Self {
1933 Self {
1934 inner: WorkspaceInner::Custom(backend),
1935 }
1936 }
1937}
1938
1939impl<S, D, P, T> WorkspaceBackend for WorkspaceInner<S, D, P, T>
1940where
1941 S: StateStoreBackend,
1942 D: DatabaseBackend,
1943 P: ProviderBackend,
1944 T: ToolsetBackend,
1945{
1946 type Conversation = Conversation<S, D, P, T>;
1947
1948 fn id(&self) -> WorkspaceId {
1949 match self {
1950 WorkspaceInner::Codex(backend) => backend.id(),
1951 WorkspaceInner::OpenCode(backend) => backend.id(),
1952 WorkspaceInner::Custom(backend) => backend.id(),
1953 }
1954 }
1955
1956 async fn info(&self) -> Result<WorkspaceInfo, KitError> {
1957 match self {
1958 WorkspaceInner::Codex(backend) => backend.info().await,
1959 WorkspaceInner::OpenCode(backend) => backend.info().await,
1960 WorkspaceInner::Custom(backend) => backend.info().await,
1961 }
1962 }
1963
1964 async fn new_conversation(
1965 &self,
1966 request: NewConversation,
1967 ) -> Result<Conversation<S, D, P, T>, KitError> {
1968 match self {
1969 WorkspaceInner::Codex(backend) => backend
1970 .new_conversation(request)
1971 .await
1972 .map(Conversation::codex),
1973 WorkspaceInner::OpenCode(backend) => backend
1974 .new_conversation(request)
1975 .await
1976 .map(Conversation::opencode),
1977 WorkspaceInner::Custom(backend) => backend
1978 .new_conversation(request)
1979 .await
1980 .map(Conversation::custom),
1981 }
1982 }
1983
1984 async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
1985 match self {
1986 WorkspaceInner::Codex(backend) => backend.list_conversations().await,
1987 WorkspaceInner::OpenCode(backend) => backend.list_conversations().await,
1988 WorkspaceInner::Custom(backend) => backend.list_conversations().await,
1989 }
1990 }
1991
1992 async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
1993 match self {
1994 WorkspaceInner::Codex(backend) => backend.conversation_info(id).await,
1995 WorkspaceInner::OpenCode(backend) => backend.conversation_info(id).await,
1996 WorkspaceInner::Custom(backend) => backend.conversation_info(id).await,
1997 }
1998 }
1999
2000 async fn get_conversation(
2001 &self,
2002 id: ConversationId,
2003 ) -> Result<Conversation<S, D, P, T>, KitError> {
2004 match self {
2005 WorkspaceInner::Codex(backend) => {
2006 backend.get_conversation(id).await.map(Conversation::codex)
2007 }
2008 WorkspaceInner::OpenCode(backend) => backend
2009 .get_conversation(id)
2010 .await
2011 .map(Conversation::opencode),
2012 WorkspaceInner::Custom(backend) => {
2013 backend.get_conversation(id).await.map(Conversation::custom)
2014 }
2015 }
2016 }
2017
2018 async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
2019 match self {
2020 WorkspaceInner::Codex(backend) => backend.list_tools().await,
2021 WorkspaceInner::OpenCode(backend) => backend.list_tools().await,
2022 WorkspaceInner::Custom(backend) => backend.list_tools().await,
2023 }
2024 }
2025
2026 async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
2027 match self {
2028 WorkspaceInner::Codex(backend) => backend.ingest_knowledge(request).await,
2029 WorkspaceInner::OpenCode(backend) => backend.ingest_knowledge(request).await,
2030 WorkspaceInner::Custom(backend) => backend.ingest_knowledge(request).await,
2031 }
2032 }
2033
2034 async fn close(self) -> Result<(), KitError> {
2035 match self {
2036 WorkspaceInner::Codex(backend) => backend.close().await,
2037 WorkspaceInner::OpenCode(backend) => backend.close().await,
2038 WorkspaceInner::Custom(backend) => backend.close().await,
2039 }
2040 }
2041}
2042
2043impl<S, D, P, T> WorkspaceBackend for Workspace<S, D, P, T>
2044where
2045 S: StateStoreBackend,
2046 D: DatabaseBackend,
2047 P: ProviderBackend,
2048 T: ToolsetBackend,
2049{
2050 type Conversation = Conversation<S, D, P, T>;
2051
2052 fn id(&self) -> WorkspaceId {
2053 self.inner.id()
2054 }
2055
2056 async fn info(&self) -> Result<WorkspaceInfo, KitError> {
2057 self.inner.info().await.map_err(|error| {
2058 error
2059 .at_operation(crate::error::Operation::WorkspaceInfo)
2060 .in_phase(crate::error::ErrorPhase::StateRead)
2061 })
2062 }
2063
2064 async fn new_conversation(
2065 &self,
2066 request: NewConversation,
2067 ) -> Result<Conversation<S, D, P, T>, KitError> {
2068 self.inner.new_conversation(request).await.map_err(|error| {
2069 error
2070 .at_operation(crate::error::Operation::NewConversation)
2071 .in_phase(crate::error::ErrorPhase::Admission)
2072 })
2073 }
2074
2075 async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
2076 self.inner.list_conversations().await.map_err(|error| {
2077 error
2078 .at_operation(crate::error::Operation::ListConversations)
2079 .in_phase(crate::error::ErrorPhase::StateRead)
2080 })
2081 }
2082
2083 async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
2084 self.inner.conversation_info(id).await.map_err(|error| {
2085 error
2086 .at_operation(crate::error::Operation::ConversationInfo)
2087 .in_phase(crate::error::ErrorPhase::StateRead)
2088 })
2089 }
2090
2091 async fn get_conversation(
2092 &self,
2093 id: ConversationId,
2094 ) -> Result<Conversation<S, D, P, T>, KitError> {
2095 self.inner.get_conversation(id).await.map_err(|error| {
2096 error
2097 .at_operation(crate::error::Operation::GetConversation)
2098 .in_phase(crate::error::ErrorPhase::Admission)
2099 })
2100 }
2101
2102 async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
2103 self.inner.list_tools().await.map_err(|error| {
2104 error
2105 .at_operation(crate::error::Operation::ListTools)
2106 .in_phase(crate::error::ErrorPhase::InventoryVerification)
2107 })
2108 }
2109
2110 async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
2111 self.inner.ingest_knowledge(request).await.map_err(|error| {
2112 error
2113 .at_operation(crate::error::Operation::IngestKnowledge)
2114 .in_phase(crate::error::ErrorPhase::KnowledgeWrite)
2115 })
2116 }
2117
2118 async fn close(self) -> Result<(), KitError> {
2119 self.inner.close().await.map_err(|error| {
2120 error
2121 .at_operation(crate::error::Operation::CloseWorkspace)
2122 .in_phase(crate::error::ErrorPhase::Lifecycle)
2123 })
2124 }
2125}