Skip to main content

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    /// See [`WorkspaceRoot`] for construction and inspection.
354    pub fn new(path: PathBuf, access: WorkspaceRootAccess) -> Result<Self, KitError> {
355        Ok(Self {
356            path: BackendHostPath::parse_absolute("workspace root", path)?,
357            access,
358        })
359    }
360
361    pub(crate) fn relative_native_file(&self, path: &Path) -> Result<PathBuf, KitError> {
362        self.path.relative_native_file(path)
363    }
364
365    /// Returns the normalized absolute backend-host path.
366    #[must_use]
367    ///
368    /// See [`WorkspaceRoot`] for construction and inspection.
369    pub fn path(&self) -> &Path {
370        self.path.as_path()
371    }
372
373    /// Returns the declared access level.
374    #[must_use]
375    ///
376    /// ```
377    /// use agents_kit::workspace::{WorkspaceRoot, WorkspaceRootAccess};
378    /// let value = WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?;
379    /// assert_eq!(value.access(), WorkspaceRootAccess::ReadOnly);
380    /// # Ok::<(), agents_kit::KitError>(())
381    /// ```
382    pub fn access(&self) -> WorkspaceRootAccess {
383        self.access
384    }
385}
386
387/// Filesystem confinement selected for every model-visible workspace action.
388#[derive(Clone, Copy, Debug, Eq, PartialEq)]
389pub enum WorkspaceSandboxMode {
390    /// Allows reads only through every installed action path.
391    ReadOnly,
392    /// Allows writes only beneath roots declared read-write.
393    WorkspaceWrite,
394    /// Allows the backend's unrestricted filesystem behavior.
395    FullAccess,
396}
397
398/// Arbitrary network access selected for model-visible workspace actions.
399#[derive(Clone, Copy, Debug, Eq, PartialEq)]
400pub enum WorkspaceNetworkAccess {
401    /// Forbids model-visible arbitrary network access.
402    Disabled,
403    /// Permits model-visible network access.
404    Enabled,
405}
406
407/// Complete execution policy for one workspace.
408#[derive(Clone, Copy, Debug, Eq, PartialEq)]
409///
410/// ```
411/// use agents_kit::workspace::{WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
412/// let value = WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled);
413/// assert_eq!(value.sandbox_mode(), WorkspaceSandboxMode::ReadOnly);
414/// # Ok::<(), agents_kit::KitError>(())
415/// ```
416pub struct WorkspaceExecutionPolicy {
417    sandbox_mode: WorkspaceSandboxMode,
418    network_access: WorkspaceNetworkAccess,
419}
420
421impl WorkspaceExecutionPolicy {
422    /// Constructs an explicit workspace execution policy.
423    #[must_use]
424    ///
425    /// See [`WorkspaceExecutionPolicy`] for construction and inspection.
426    pub fn new(sandbox_mode: WorkspaceSandboxMode, network_access: WorkspaceNetworkAccess) -> Self {
427        Self {
428            sandbox_mode,
429            network_access,
430        }
431    }
432
433    /// Returns the selected filesystem sandbox mode.
434    #[must_use]
435    ///
436    /// See [`WorkspaceExecutionPolicy`] for construction and inspection.
437    pub fn sandbox_mode(&self) -> WorkspaceSandboxMode {
438        self.sandbox_mode
439    }
440
441    /// Returns the selected network access policy.
442    #[must_use]
443    ///
444    /// ```
445    /// use agents_kit::workspace::{WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
446    /// let value = WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled);
447    /// assert_eq!(value.network_access(), WorkspaceNetworkAccess::Disabled);
448    /// # Ok::<(), agents_kit::KitError>(())
449    /// ```
450    pub fn network_access(&self) -> WorkspaceNetworkAccess {
451        self.network_access
452    }
453}
454
455#[derive(Clone, Debug, Eq, PartialEq)]
456/// Declares the complete portable context for a workspace.
457///
458/// ```
459/// use agents_kit::workspace::{
460///     WorkspaceContext, WorkspaceExecutionPolicy, WorkspaceNetworkAccess,
461///     WorkspaceRoot, WorkspaceRootAccess, WorkspaceSandboxMode,
462/// };
463///
464/// let cwd = std::env::current_dir().expect("current directory");
465/// let root = WorkspaceRoot::new(cwd.clone(), WorkspaceRootAccess::ReadWrite)?;
466/// let policy = WorkspaceExecutionPolicy::new(
467///     WorkspaceSandboxMode::FullAccess,
468///     WorkspaceNetworkAccess::Enabled,
469/// );
470/// let context = WorkspaceContext::new(cwd.clone(), vec![root], policy)?;
471/// assert_eq!(context.cwd(), cwd);
472/// # Ok::<(), agents_kit::KitError>(())
473/// ```
474pub struct WorkspaceContext {
475    cwd: BackendHostPath,
476    roots: Vec<WorkspaceRoot>,
477    execution_policy: WorkspaceExecutionPolicy,
478}
479
480impl WorkspaceContext {
481    /// Validates and constructs a complete passive workspace context.
482    ///
483    /// # Errors
484    /// 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.
485    ///
486    /// ```
487    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
488    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
489    /// let context = WorkspaceContext::new("/workspace".into(),
490    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
491    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
492    /// assert_eq!(context.roots().len(), 1);
493    /// # Ok::<(), agents_kit::KitError>(())
494    /// ```
495    pub fn new(
496        cwd: PathBuf,
497        roots: Vec<WorkspaceRoot>,
498        execution_policy: WorkspaceExecutionPolicy,
499    ) -> Result<Self, KitError> {
500        let cwd = BackendHostPath::parse_absolute("workspace cwd", cwd)?;
501        if roots.is_empty() {
502            return Err(KitError::invalid_input(
503                "workspace context requires at least one root",
504            ));
505        }
506
507        let mut deduplicated = Vec::<WorkspaceRoot>::new();
508        for root in roots {
509            if !cwd.same_flavor(&root.path) {
510                return Err(KitError::invalid_input(
511                    "workspace cwd and roots must use one backend-host path flavor",
512                ));
513            }
514            if let Some(existing) = deduplicated
515                .iter()
516                .find(|existing| existing.path == root.path)
517            {
518                if existing.access != root.access {
519                    return Err(KitError::invalid_input(
520                        "duplicate workspace roots must not contradict access",
521                    ));
522                }
523                continue;
524            }
525            if deduplicated.iter().any(|existing| {
526                existing.path.overlaps(&root.path) && existing.access != root.access
527            }) {
528                return Err(KitError::invalid_input(
529                    "overlapping workspace roots must not contradict access",
530                ));
531            }
532            deduplicated.push(root);
533        }
534
535        let containing = deduplicated
536            .iter()
537            .filter(|root| root.path.contains(&cwd))
538            .collect::<Vec<_>>();
539        if containing.is_empty() {
540            return Err(KitError::invalid_input(
541                "workspace cwd must be contained by a declared root",
542            ));
543        }
544
545        match execution_policy.sandbox_mode {
546            WorkspaceSandboxMode::ReadOnly
547                if deduplicated
548                    .iter()
549                    .any(|root| root.access != WorkspaceRootAccess::ReadOnly) =>
550            {
551                return Err(KitError::invalid_input(
552                    "read-only workspace policy requires every root to be read-only",
553                ));
554            }
555            WorkspaceSandboxMode::WorkspaceWrite
556                if containing
557                    .iter()
558                    .any(|root| root.access != WorkspaceRootAccess::ReadWrite) =>
559            {
560                return Err(KitError::invalid_input(
561                    "workspace-write policy requires the cwd root to be read-write",
562                ));
563            }
564            WorkspaceSandboxMode::FullAccess
565                if deduplicated
566                    .iter()
567                    .any(|root| root.access != WorkspaceRootAccess::ReadWrite) =>
568            {
569                return Err(KitError::invalid_input(
570                    "full-access workspace policy requires every root to be read-write",
571                ));
572            }
573            _ => {}
574        }
575
576        Ok(Self {
577            cwd,
578            roots: deduplicated,
579            execution_policy,
580        })
581    }
582
583    /// Returns the absolute backend-host cwd.
584    ///
585    /// ```
586    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
587    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
588    /// let context = WorkspaceContext::new("/workspace".into(),
589    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
590    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
591    /// assert_eq!(context.cwd(), std::path::Path::new("/workspace"));
592    /// # Ok::<(), agents_kit::KitError>(())
593    /// ```
594    pub fn cwd(&self) -> &Path {
595        self.cwd.as_path()
596    }
597
598    /// Returns the deduplicated declared workspace roots.
599    #[must_use]
600    ///
601    /// ```
602    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
603    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
604    /// let context = WorkspaceContext::new("/workspace".into(),
605    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
606    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
607    /// assert_eq!(context.roots()[0].access(), WorkspaceRootAccess::ReadOnly);
608    /// # Ok::<(), agents_kit::KitError>(())
609    /// ```
610    pub fn roots(&self) -> &[WorkspaceRoot] {
611        &self.roots
612    }
613
614    /// Returns the complete execution policy.
615    #[must_use]
616    ///
617    /// ```
618    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
619    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
620    /// let context = WorkspaceContext::new("/workspace".into(),
621    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
622    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
623    /// assert_eq!(context.execution_policy().network_access(), WorkspaceNetworkAccess::Disabled);
624    /// # Ok::<(), agents_kit::KitError>(())
625    /// ```
626    pub fn execution_policy(&self) -> &WorkspaceExecutionPolicy {
627        &self.execution_policy
628    }
629
630    pub(crate) fn normalized_relative(path: &Path) -> Result<PathBuf, KitError> {
631        BackendHostPath::validate_portable_relative(path)
632    }
633
634    pub(crate) fn path_flavor(&self) -> BackendHostPathFlavor {
635        self.cwd.anchor.flavor()
636    }
637
638    pub(crate) fn resolve(&self, path: &Path) -> Result<(PathBuf, PathBuf), KitError> {
639        let relative = Self::normalized_relative(path)?;
640        let absolute = self.cwd.resolve(&relative)?;
641        if !self.roots.iter().any(|root| root.path.contains(&absolute)) {
642            return Err(KitError::invalid_input(
643                "workspace path must remain beneath a declared root",
644            ));
645        }
646        Ok((relative, absolute.into_path_buf()))
647    }
648}
649
650#[derive(Clone, Debug, Eq, PartialEq)]
651/// Supplies optional model, reasoning, and knowledge defaults for a workspace.
652///
653/// ```
654/// use agents_kit::workspace::WorkspaceDefaults;
655/// let value = WorkspaceDefaults::new(None, None, None)?;
656/// assert_eq!(value.model(), None);
657/// # Ok::<(), agents_kit::KitError>(())
658/// ```
659pub struct WorkspaceDefaults {
660    model: Option<ModelSelector>,
661    personality: Option<PersonalitySelector>,
662    reasoning_effort: Option<ReasoningEffort>,
663    knowledge_policy: Option<KnowledgePolicy>,
664}
665
666impl WorkspaceDefaults {
667    /// Constructs validated workspace defaults.
668    ///
669    /// # Errors
670    /// Returns InvalidInput for an invalid explicit knowledge scope selection. Model and personality availability is checked by the receiving backend.
671    ///
672    /// See [`WorkspaceDefaults`] for construction and inspection.
673    pub fn new(
674        model: Option<ModelSelector>,
675        reasoning_effort: Option<ReasoningEffort>,
676        knowledge_policy: Option<KnowledgePolicy>,
677    ) -> Result<Self, KitError> {
678        if let Some(policy) = &knowledge_policy {
679            policy.validate()?;
680        }
681        Ok(Self {
682            model,
683            personality: None,
684            reasoning_effort,
685            knowledge_policy,
686        })
687    }
688
689    /// Constructs defaults with every optional value absent.
690    ///
691    /// ```
692    /// use agents_kit::workspace::WorkspaceDefaults;
693    /// let value = WorkspaceDefaults::new(None, None, None)?;
694    /// let value = WorkspaceDefaults::empty();
695    /// assert_eq!(value.knowledge_policy(), None);
696    /// # Ok::<(), agents_kit::KitError>(())
697    /// ```
698    pub fn empty() -> Self {
699        Self {
700            model: None,
701            personality: None,
702            reasoning_effort: None,
703            knowledge_policy: None,
704        }
705    }
706
707    /// Returns the optional model default.
708    ///
709    /// See [`WorkspaceDefaults`] for construction and inspection.
710    pub fn model(&self) -> Option<&ModelSelector> {
711        self.model.as_ref()
712    }
713
714    /// Sets the optional personality default.
715    #[must_use]
716    ///
717    /// ```
718    /// use agents_kit::workspace::WorkspaceDefaults;
719    /// let value = WorkspaceDefaults::new(None, None, None)?;
720    /// let value = value.with_personality(agents_kit::model::PersonalitySelector::new("concise")?);
721    /// assert_eq!(value.personality().unwrap().as_str(), "concise");
722    /// # Ok::<(), agents_kit::KitError>(())
723    /// ```
724    pub fn with_personality(mut self, personality: PersonalitySelector) -> Self {
725        self.personality = Some(personality);
726        self
727    }
728
729    /// Returns the optional personality default.
730    #[must_use]
731    ///
732    /// ```
733    /// use agents_kit::workspace::WorkspaceDefaults;
734    /// let value = WorkspaceDefaults::new(None, None, None)?;
735    /// assert_eq!(value.personality(), None);
736    /// # Ok::<(), agents_kit::KitError>(())
737    /// ```
738    pub fn personality(&self) -> Option<&PersonalitySelector> {
739        self.personality.as_ref()
740    }
741
742    /// Returns the optional reasoning-effort default.
743    ///
744    /// ```
745    /// use agents_kit::workspace::WorkspaceDefaults;
746    /// let value = WorkspaceDefaults::new(None, None, None)?;
747    /// assert_eq!(value.reasoning_effort(), None);
748    /// # Ok::<(), agents_kit::KitError>(())
749    /// ```
750    pub fn reasoning_effort(&self) -> Option<&ReasoningEffort> {
751        self.reasoning_effort.as_ref()
752    }
753
754    /// Returns the optional managed-knowledge policy default.
755    ///
756    /// ```
757    /// use agents_kit::workspace::WorkspaceDefaults;
758    /// let value = WorkspaceDefaults::new(None, None, None)?;
759    /// assert_eq!(value.knowledge_policy(), None);
760    /// # Ok::<(), agents_kit::KitError>(())
761    /// ```
762    pub fn knowledge_policy(&self) -> Option<&KnowledgePolicy> {
763        self.knowledge_policy.as_ref()
764    }
765}
766
767#[derive(Clone, Debug, Eq, PartialEq)]
768/// Describes one workspace to create.
769///
770/// ```
771/// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
772///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
773/// let context = WorkspaceContext::new("/workspace".into(),
774///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
775///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
776/// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
777/// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
778/// assert_eq!(value.name(), "review");
779/// # Ok::<(), agents_kit::KitError>(())
780/// ```
781pub struct NewWorkspace {
782    name: String,
783    context: WorkspaceContext,
784    defaults: WorkspaceDefaults,
785}
786
787impl NewWorkspace {
788    /// Validates and constructs a new-workspace request.
789    ///
790    /// # Errors
791    /// Returns InvalidInput for an empty or padded name.
792    ///
793    /// See [`NewWorkspace`] for construction and inspection.
794    pub fn new(
795        name: impl Into<String>,
796        context: WorkspaceContext,
797        defaults: WorkspaceDefaults,
798    ) -> Result<Self, KitError> {
799        let name = name.into();
800        Self::validate_name(&name)?;
801        Ok(Self {
802            name,
803            context,
804            defaults,
805        })
806    }
807
808    /// Returns the workspace name.
809    ///
810    /// See [`NewWorkspace`] for construction and inspection.
811    pub fn name(&self) -> &str {
812        &self.name
813    }
814
815    /// Returns the complete workspace context.
816    ///
817    /// ```
818    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
819    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
820    /// let context = WorkspaceContext::new("/workspace".into(),
821    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
822    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
823    /// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
824    /// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
825    /// assert_eq!(value.context().cwd(), std::path::Path::new("/workspace"));
826    /// # Ok::<(), agents_kit::KitError>(())
827    /// ```
828    pub fn context(&self) -> &WorkspaceContext {
829        &self.context
830    }
831
832    /// Returns the complete workspace defaults.
833    ///
834    /// ```
835    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
836    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
837    /// let context = WorkspaceContext::new("/workspace".into(),
838    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
839    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
840    /// use agents_kit::workspace::{NewWorkspace, WorkspaceDefaults};
841    /// let value = NewWorkspace::new("review", context, WorkspaceDefaults::empty())?;
842    /// assert_eq!(value.defaults().model(), None);
843    /// # Ok::<(), agents_kit::KitError>(())
844    /// ```
845    pub fn defaults(&self) -> &WorkspaceDefaults {
846        &self.defaults
847    }
848
849    fn validate_name(name: &str) -> Result<(), KitError> {
850        if name.is_empty() || name.trim() != name {
851            return Err(KitError::invalid_input(
852                "workspace name must be nonempty and unpadded",
853            ));
854        }
855        Ok(())
856    }
857}
858
859#[derive(Clone, Debug, Eq, PartialEq)]
860/// Contains one committed workspace snapshot.
861///
862/// ```
863/// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
864///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
865/// let context = WorkspaceContext::new("/workspace".into(),
866///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
867///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
868/// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
869/// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
870/// assert_eq!(value.id().as_str(), "workspace_00000000000000000000000000000000");
871/// # Ok::<(), agents_kit::KitError>(())
872/// ```
873pub struct WorkspaceInfo {
874    id: WorkspaceId,
875    name: String,
876    context: WorkspaceContext,
877    defaults: WorkspaceDefaults,
878    revision: u64,
879}
880
881impl WorkspaceInfo {
882    /// Validates and constructs one committed workspace snapshot.
883    ///
884    /// The name must be nonempty and unpadded, and `revision` must be
885    /// nonzero.
886    ///
887    /// # Errors
888    ///
889    /// Returns [`crate::error::ErrorKind::InvalidInput`] for an invalid name or
890    /// [`crate::error::ErrorKind::InvalidState`] for a zero revision.
891    ///
892    /// # Examples
893    ///
894    /// ```
895    /// use agents_kit::{
896    ///     identity::WorkspaceId,
897    ///     workspace::{
898    ///     WorkspaceContext, WorkspaceDefaults, WorkspaceExecutionPolicy,
899    ///     WorkspaceInfo, WorkspaceNetworkAccess, WorkspaceRoot,
900    ///     WorkspaceRootAccess, WorkspaceSandboxMode,
901    ///     },
902    /// };
903    ///
904    /// let cwd = std::env::current_dir()?;
905    /// let context = WorkspaceContext::new(
906    ///     cwd.clone(),
907    ///     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadWrite)?],
908    ///     WorkspaceExecutionPolicy::new(
909    ///         WorkspaceSandboxMode::FullAccess,
910    ///         WorkspaceNetworkAccess::Enabled,
911    ///     ),
912    /// )?;
913    /// let id: WorkspaceId =
914    ///     "workspace_00000000000000000000000000000000".parse()?;
915    /// let info = WorkspaceInfo::new(
916    ///     id,
917    ///     "release".to_owned(),
918    ///     context,
919    ///     WorkspaceDefaults::empty(),
920    ///     1,
921    /// )?;
922    /// assert_eq!(info.revision(), 1);
923    /// # Ok::<(), Box<dyn std::error::Error>>(())
924    /// ```
925    pub fn new(
926        id: WorkspaceId,
927        name: String,
928        context: WorkspaceContext,
929        defaults: WorkspaceDefaults,
930        revision: u64,
931    ) -> Result<Self, KitError> {
932        NewWorkspace::validate_name(&name)?;
933        if revision == 0 {
934            return Err(KitError::invalid_state(
935                "workspace revision must be nonzero",
936            ));
937        }
938        Ok(Self {
939            id,
940            name,
941            context,
942            defaults,
943            revision,
944        })
945    }
946
947    /// Returns the portable workspace identity.
948    ///
949    /// See [`WorkspaceInfo`] for construction and inspection.
950    pub fn id(&self) -> &WorkspaceId {
951        &self.id
952    }
953
954    /// Returns the committed workspace name.
955    ///
956    /// ```
957    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
958    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
959    /// let context = WorkspaceContext::new("/workspace".into(),
960    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
961    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
962    /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
963    /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
964    /// assert_eq!(value.name(), "review");
965    /// # Ok::<(), agents_kit::KitError>(())
966    /// ```
967    pub fn name(&self) -> &str {
968        &self.name
969    }
970
971    /// Returns the committed workspace context.
972    ///
973    /// ```
974    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
975    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
976    /// let context = WorkspaceContext::new("/workspace".into(),
977    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
978    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
979    /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
980    /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
981    /// assert_eq!(value.context().cwd(), std::path::Path::new("/workspace"));
982    /// # Ok::<(), agents_kit::KitError>(())
983    /// ```
984    pub fn context(&self) -> &WorkspaceContext {
985        &self.context
986    }
987
988    /// Returns the committed workspace defaults.
989    ///
990    /// ```
991    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
992    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
993    /// let context = WorkspaceContext::new("/workspace".into(),
994    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
995    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
996    /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
997    /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
998    /// assert_eq!(value.defaults().model(), None);
999    /// # Ok::<(), agents_kit::KitError>(())
1000    /// ```
1001    pub fn defaults(&self) -> &WorkspaceDefaults {
1002        &self.defaults
1003    }
1004
1005    /// Returns the nonzero committed revision.
1006    ///
1007    /// ```
1008    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1009    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1010    /// let context = WorkspaceContext::new("/workspace".into(),
1011    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1012    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1013    /// use agents_kit::workspace::{WorkspaceInfo, WorkspaceDefaults};
1014    /// let value = WorkspaceInfo::new("workspace_00000000000000000000000000000000".parse()?, "review".into(), context, WorkspaceDefaults::empty(), 1)?;
1015    /// assert_eq!(value.revision(), 1);
1016    /// # Ok::<(), agents_kit::KitError>(())
1017    /// ```
1018    pub fn revision(&self) -> u64 {
1019        self.revision
1020    }
1021}
1022
1023#[derive(Clone, Debug, Eq, PartialEq)]
1024/// Proposes a revision-checked complete workspace replacement.
1025///
1026/// ```
1027/// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1028///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1029/// let context = WorkspaceContext::new("/workspace".into(),
1030///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1031///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1032/// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1033/// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1034/// assert_eq!(value.expected_revision(), 1);
1035/// # Ok::<(), agents_kit::KitError>(())
1036/// ```
1037pub struct UpdateWorkspaceRequest {
1038    id: WorkspaceId,
1039    expected_revision: u64,
1040    name: String,
1041    context: WorkspaceContext,
1042    defaults: WorkspaceDefaults,
1043}
1044
1045impl UpdateWorkspaceRequest {
1046    /// Validates and constructs a workspace update request.
1047    ///
1048    /// # Errors
1049    /// Returns InvalidInput for an empty/padded name or zero expected revision. Constructing a request does not reserve or change that revision.
1050    ///
1051    /// See [`UpdateWorkspaceRequest`] for construction and inspection.
1052    pub fn new(
1053        id: WorkspaceId,
1054        expected_revision: u64,
1055        name: impl Into<String>,
1056        context: WorkspaceContext,
1057        defaults: WorkspaceDefaults,
1058    ) -> Result<Self, KitError> {
1059        let name = name.into();
1060        NewWorkspace::validate_name(&name)?;
1061        if expected_revision == 0 {
1062            return Err(KitError::invalid_input(
1063                "expected workspace revision must be nonzero",
1064            ));
1065        }
1066        Ok(Self {
1067            id,
1068            expected_revision,
1069            name,
1070            context,
1071            defaults,
1072        })
1073    }
1074
1075    /// Returns the workspace identity to update.
1076    ///
1077    /// ```
1078    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1079    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1080    /// let context = WorkspaceContext::new("/workspace".into(),
1081    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1082    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1083    /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1084    /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1085    /// assert_eq!(value.id().as_str(), "workspace_00000000000000000000000000000000");
1086    /// # Ok::<(), agents_kit::KitError>(())
1087    /// ```
1088    pub fn id(&self) -> &WorkspaceId {
1089        &self.id
1090    }
1091
1092    /// Returns the revision that must still be current.
1093    ///
1094    /// See [`UpdateWorkspaceRequest`] for construction and inspection.
1095    pub fn expected_revision(&self) -> u64 {
1096        self.expected_revision
1097    }
1098
1099    /// Returns the proposed replacement name.
1100    ///
1101    /// ```
1102    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1103    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1104    /// let context = WorkspaceContext::new("/workspace".into(),
1105    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1106    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1107    /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1108    /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1109    /// assert_eq!(value.name(), "review");
1110    /// # Ok::<(), agents_kit::KitError>(())
1111    /// ```
1112    pub fn name(&self) -> &str {
1113        &self.name
1114    }
1115
1116    /// Returns the proposed replacement context.
1117    ///
1118    /// ```
1119    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1120    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1121    /// let context = WorkspaceContext::new("/workspace".into(),
1122    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1123    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1124    /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1125    /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1126    /// assert_eq!(value.context().cwd(), std::path::Path::new("/workspace"));
1127    /// # Ok::<(), agents_kit::KitError>(())
1128    /// ```
1129    pub fn context(&self) -> &WorkspaceContext {
1130        &self.context
1131    }
1132
1133    /// Returns the proposed replacement defaults.
1134    ///
1135    /// ```
1136    /// use agents_kit::workspace::{WorkspaceContext, WorkspaceRoot, WorkspaceRootAccess,
1137    ///     WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1138    /// let context = WorkspaceContext::new("/workspace".into(),
1139    ///     vec![WorkspaceRoot::new("/workspace".into(), WorkspaceRootAccess::ReadOnly)?],
1140    ///     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1141    /// use agents_kit::workspace::{UpdateWorkspaceRequest, WorkspaceDefaults};
1142    /// let value = UpdateWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1, "review", context, WorkspaceDefaults::empty())?;
1143    /// assert_eq!(value.defaults().model(), None);
1144    /// # Ok::<(), agents_kit::KitError>(())
1145    /// ```
1146    pub fn defaults(&self) -> &WorkspaceDefaults {
1147        &self.defaults
1148    }
1149}
1150
1151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1152/// Reports advisory readiness for a workspace update.
1153pub enum WorkspaceUpdateReadiness {
1154    /// The current snapshot has no known blocker.
1155    Ready,
1156    /// Current admitted or descendant work blocks mutation.
1157    Busy,
1158}
1159
1160#[derive(Clone, Debug, Eq, PartialEq)]
1161/// Requests revision-checked deletion of one quiescent workspace.
1162///
1163/// ```
1164/// use agents_kit::workspace::DeleteWorkspaceRequest;
1165/// let value = DeleteWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1)?;
1166/// assert_eq!(value.expected_revision(), 1);
1167/// # Ok::<(), agents_kit::KitError>(())
1168/// ```
1169pub struct DeleteWorkspaceRequest {
1170    id: WorkspaceId,
1171    expected_revision: u64,
1172}
1173
1174impl DeleteWorkspaceRequest {
1175    /// Validates and constructs a workspace deletion request.
1176    ///
1177    /// # Errors
1178    /// Returns InvalidInput for a zero expected revision. Actual deletion also requires current revision and quiescence.
1179    ///
1180    /// See [`DeleteWorkspaceRequest`] for construction and inspection.
1181    pub fn new(id: WorkspaceId, expected_revision: u64) -> Result<Self, KitError> {
1182        if expected_revision == 0 {
1183            return Err(KitError::invalid_input(
1184                "expected workspace revision must be nonzero",
1185            ));
1186        }
1187        Ok(Self {
1188            id,
1189            expected_revision,
1190        })
1191    }
1192
1193    /// Returns the workspace identity to delete.
1194    ///
1195    /// ```
1196    /// use agents_kit::workspace::DeleteWorkspaceRequest;
1197    /// let value = DeleteWorkspaceRequest::new("workspace_00000000000000000000000000000000".parse()?, 1)?;
1198    /// assert_eq!(value.id().as_str(), "workspace_00000000000000000000000000000000");
1199    /// # Ok::<(), agents_kit::KitError>(())
1200    /// ```
1201    pub fn id(&self) -> &WorkspaceId {
1202        &self.id
1203    }
1204
1205    /// Returns the revision that must still be current.
1206    ///
1207    /// See [`DeleteWorkspaceRequest`] for construction and inspection.
1208    pub fn expected_revision(&self) -> u64 {
1209        self.expected_revision
1210    }
1211}
1212
1213/// Public implementation contract for one revision-bound portable workspace.
1214///
1215/// A caller Agent returns its own concrete workspace through
1216/// [`AgentBackend::Workspace`](crate::AgentBackend::Workspace). Each workspace owns
1217/// its associated [`Conversation`](Self::Conversation); runtime composition may use
1218/// an exhaustive caller enum hierarchy. A stable workspace ID does not refresh a
1219/// stale runtime revision. Closing invalidates runtime descendants while preserving
1220/// quiescent durable records for direct-ID reopen.
1221///
1222/// See [`crate::AgentBackend::new_workspace`] for creation and
1223/// [`Self::new_conversation`] for the child lifecycle. The [executable caller hierarchy](https://github.com/jecsand838/ai-kit/blob/main/agents-kit/tests/mock_hierarchy/mod.rs)
1224/// implements these contracts directly without a provider, process or database.
1225pub trait WorkspaceBackend: Send + Sync + 'static {
1226    /// Concrete conversation implementation produced by this workspace.
1227    type Conversation: crate::ConversationBackend;
1228
1229    /// Returns the durable portable workspace identity.
1230    ///
1231    /// This is the durable workspace identity. A handle also carries a runtime
1232    /// revision binding, so a stable id does not make an old handle current.
1233    ///
1234    /// See [`Self::new_conversation`] for the complete child lifecycle and [`crate::AgentBackend::new_workspace`] for workspace creation.
1235    fn id(&self) -> WorkspaceId;
1236
1237    /// Reads the current committed workspace information.
1238    ///
1239    /// The result contains the complete committed context/defaults and revision.
1240    /// It does not refresh this handle after a concurrent revision change.
1241    ///
1242    /// # Errors
1243    ///
1244    /// Returns [`KitError`] for a closed parent/workspace, stale revision,
1245    /// state-read failure, inconsistent durable state, or deadline expiry.
1246    ///
1247    /// See [`Self::new_conversation`] for the complete child lifecycle and [`crate::AgentBackend::new_workspace`] for workspace creation.
1248    fn info(&self) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
1249
1250    /// Creates and publishes one conversation under this workspace.
1251    ///
1252    /// The returned associated child is published only after resolved defaults,
1253    /// initial knowledge, backend attachment, and durable ownership agree.
1254    ///
1255    /// # Errors
1256    ///
1257    /// Returns [`KitError`] for closed or stale runtime, busy admission, invalid
1258    /// input or unsupported policy/integrations, and dependency/deadline failures.
1259    /// Accepted native setup remains durably attached or recovery-owned on failure.
1260    ///
1261    /// # Examples
1262    ///
1263    /// This local Custom agent uses in-memory Lance and sends no network requests.
1264    ///
1265    /// ```standalone_crate
1266    /// #![recursion_limit = "256"] // Nested concrete async futures need this on Linux.
1267    /// # use std::{num::NonZeroUsize, time::Duration};
1268    /// # use agents_kit::{AgentBackend, AgentBuilder, AgentStorage, WorkspaceBackend};
1269    /// # use agents_kit::config::{AgentConfig, CustomAgentConfig, CustomLoopLimits, CustomToolApproval};
1270    /// # use agents_kit::{model::ModelSelector, tool::ToolSelection};
1271    /// # use providers_kit::{Provider, ProviderConfig, config::{OllamaConfig, ProviderKind, TransportConfig}};
1272    /// # use storage_kit::{StateStore, state::config::{LanceStateStoreConfig, StateStoreConfig}};
1273    /// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
1274    /// # runtime.block_on(async {
1275    /// # let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
1276    /// # let custom = CustomAgentConfig::new(ModelSelector::new("exact-model")?, ToolSelection::None,
1277    /// #     CustomLoopLimits::new(4, 0, 1)?, CustomToolApproval::Deny)?;
1278    /// # let config = AgentConfig::new(custom.into(), Duration::from_secs(10), NonZeroUsize::MIN)?;
1279    /// # let provider = Provider::from_config(ProviderConfig::new(
1280    /// #     ProviderKind::Ollama(OllamaConfig::local()), TransportConfig::default()))?;
1281    /// # let agent = AgentBuilder::create(config, AgentStorage::new(store)).with_provider(provider).build().await?;
1282    /// # let outcome = async {
1283    /// # use agents_kit::workspace::{NewWorkspace, WorkspaceContext, WorkspaceDefaults,
1284    /// #     WorkspaceRoot, WorkspaceRootAccess, WorkspaceExecutionPolicy, WorkspaceSandboxMode, WorkspaceNetworkAccess};
1285    /// # let cwd = std::env::current_dir()?;
1286    /// # let context = WorkspaceContext::new(cwd.clone(),
1287    /// #     vec![WorkspaceRoot::new(cwd, WorkspaceRootAccess::ReadOnly)?],
1288    /// #     WorkspaceExecutionPolicy::new(WorkspaceSandboxMode::ReadOnly, WorkspaceNetworkAccess::Disabled))?;
1289    /// # let workspace = agent.new_workspace(NewWorkspace::new("review", context, WorkspaceDefaults::empty())?).await?;
1290    /// use agents_kit::ConversationBackend;
1291    /// use agents_kit::conversation::NewConversation;
1292    /// let workspace_id = workspace.id();
1293    /// let result = async {
1294    ///     let conversation = workspace.new_conversation(NewConversation::new(
1295    ///         Some("Review".into()), None, Vec::new(),
1296    ///     )?).await?;
1297    ///     let id = conversation.id();
1298    ///     let info = conversation.info().await?;
1299    ///     assert_eq!(info.id(), &id);
1300    ///     assert_eq!(info.workspace_id(), &workspace_id);
1301    ///     assert_eq!(info.title(), Some("Review"));
1302    ///     let inventory = workspace.list_conversations().await?;
1303    ///     assert_eq!(inventory.len(), 1);
1304    ///     assert_eq!(inventory[0].id(), &id);
1305    ///     assert_eq!(workspace.conversation_info(id.clone()).await?.id(), &id);
1306    ///     assert!(workspace.list_tools().await?.is_empty());
1307    ///     Ok::<_, agents_kit::KitError>(id)
1308    /// }.await;
1309    /// // Parent close settles children and preserves quiescent records for reopen.
1310    /// let close = workspace.close().await;
1311    /// let conversation_id = match (result, close) {
1312    ///     (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
1313    ///     (Err(error), _) | (_, Err(error)) => Err(error),
1314    ///     (Ok(id), Ok(())) => Ok(id),
1315    /// }?;
1316    /// let workspace = agent.get_workspace(workspace_id.clone()).await?;
1317    /// let result = async {
1318    ///     assert_eq!(workspace.id(), workspace_id);
1319    ///     assert_eq!(workspace.info().await?.id(), &workspace_id);
1320    ///     let conversation = workspace.get_conversation(conversation_id.clone()).await?;
1321    ///     let observed = conversation.info().await;
1322    ///     // Explicit child close is terminal; unlike parent close, it ends reopenability.
1323    ///     let close = conversation.close().await;
1324    ///     let info = match (observed, close) {
1325    ///         (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
1326    ///         (Err(error), _) | (_, Err(error)) => Err(error),
1327    ///         (Ok(info), Ok(())) => Ok(info),
1328    ///     }?;
1329    ///     assert_eq!(info.id(), &conversation_id);
1330    ///     assert_eq!(info.workspace_id(), &workspace_id);
1331    ///     assert_eq!(info.title(), Some("Review"));
1332    ///     assert!(workspace.list_conversations().await?.is_empty());
1333    ///     Ok::<(), agents_kit::KitError>(())
1334    /// }.await;
1335    /// let close = workspace.close().await;
1336    /// match (result, close) {
1337    ///     (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
1338    ///     (Err(error), _) | (_, Err(error)) => Err(error),
1339    ///     (Ok(()), Ok(())) => Ok(()),
1340    /// }?;
1341    /// # Ok::<(), agents_kit::KitError>(())
1342    /// # }.await;
1343    /// # let cleanup = agent.shutdown().await;
1344    /// # match (outcome, cleanup) {
1345    /// #     (Err(primary), Err(cleanup)) => Err(primary.with_supplementary(cleanup)),
1346    /// #     (Err(error), _) | (_, Err(error)) => Err(error),
1347    /// #     (Ok(()), Ok(())) => Ok(()),
1348    /// # }?;
1349    /// # Ok::<(), Box<dyn std::error::Error>>(())
1350    /// # })?;
1351    /// # Ok::<(), Box<dyn std::error::Error>>(())
1352    /// ```
1353    fn new_conversation(
1354        &self,
1355        request: NewConversation,
1356    ) -> impl Future<Output = Result<Self::Conversation, KitError>> + Send + '_;
1357
1358    /// Lists live or reopenable conversations in deterministic identity order.
1359    ///
1360    /// Inventory comes from ownership-valid durable records; it is not limited
1361    /// to handles currently open in this process. Terminally closed conversations
1362    /// are excluded. Listing does not claim live handles.
1363    ///
1364    /// # Errors
1365    ///
1366    /// Returns [`KitError`] for closed or stale runtime, state/dependency read
1367    /// failures, inconsistent inventory, or deadline expiry.
1368    ///
1369    /// See [`Self::new_conversation`] for the complete child lifecycle and [`crate::AgentBackend::new_workspace`] for workspace creation.
1370    fn list_conversations(
1371        &self,
1372    ) -> impl Future<Output = Result<Vec<ConversationInfo>, KitError>> + Send + '_;
1373
1374    /// Reads committed information for one conversation identity.
1375    ///
1376    /// This reads committed metadata without claiming a live child handle.
1377    /// Missing and foreign identities do not reveal a foreign owner.
1378    ///
1379    /// # Errors
1380    ///
1381    /// Returns [`crate::error::ErrorKind::NotFound`] for an unavailable or foreign
1382    /// conversation, or the owning closed/stale runtime, state-read, integrity, or
1383    /// deadline error.
1384    ///
1385    /// See [`Self::new_conversation`] for the complete child lifecycle and [`crate::AgentBackend::new_workspace`] for workspace creation.
1386    fn conversation_info(
1387        &self,
1388        id: ConversationId,
1389    ) -> impl Future<Output = Result<ConversationInfo, KitError>> + Send + '_;
1390
1391    /// Opens the current live handle for one conversation identity.
1392    ///
1393    /// A current conversation has at most one live public handle; repeated lookup
1394    /// is busy until its claim is released. Reopen verifies the current workspace
1395    /// revision, defaults, attachments, tools, knowledge, and recovery state. It
1396    /// does not create a replacement session for a missing identity.
1397    ///
1398    /// # Errors
1399    ///
1400    /// Returns [`KitError`] for a missing/foreign or terminal conversation, an
1401    /// existing live claim, closed or stale runtime, incompatible reopen state,
1402    /// or dependency/deadline failure.
1403    ///
1404    /// See [`Self::new_conversation`] for the complete child lifecycle and [`crate::AgentBackend::new_workspace`] for workspace creation.
1405    fn get_conversation(
1406        &self,
1407        id: ConversationId,
1408    ) -> impl Future<Output = Result<Self::Conversation, KitError>> + Send + '_;
1409
1410    /// Lists agents-kit-managed tools installed for this workspace.
1411    ///
1412    /// Returns the complete source-neutral catalog the Agent owns and can expose
1413    /// in this workspace, subject to its execution policy. An unavailable source
1414    /// must not be silently omitted from a purportedly complete result.
1415    ///
1416    /// # Errors
1417    ///
1418    /// Returns [`KitError`] for closed or stale runtime, unsupported or incomplete
1419    /// catalog exposure, malformed source data, or dependency/deadline failure.
1420    ///
1421    /// See [`Self::new_conversation`] for the complete child lifecycle and [`crate::AgentBackend::new_workspace`] for workspace creation.
1422    fn list_tools(&self) -> impl Future<Output = Result<Vec<ToolInfo>, KitError>> + Send + '_;
1423
1424    /// Atomically replaces managed knowledge from this workspace.
1425    ///
1426    /// An omitted scope selects this Workspace. Explicit Agent or Database scope
1427    /// is validated under the shared knowledge rules; Conversation scope is not
1428    /// accepted here. Empty sources are a no-op after scope validation, not deletion.
1429    /// Updating workspace context/defaults does not move sources. See [`crate::knowledge`]
1430    /// for local ingestion and policy examples.
1431    ///
1432    /// # Errors
1433    ///
1434    /// Returns [`KitError`] for invalid sources or scope, missing/incompatible
1435    /// knowledge integrations, closed or stale runtime, busy admission, or
1436    /// dependency/deadline failure. Unsupported semantics fail explicitly.
1437    ///
1438    /// Caller implementations need no service. See [`IngestKnowledgeRequest`] for request construction and the [executable caller hierarchy](https://github.com/jecsand838/ai-kit/blob/main/agents-kit/tests/mock_hierarchy/mod.rs) for direct-trait ingestion.
1439    fn ingest_knowledge(
1440        &self,
1441        request: IngestKnowledgeRequest,
1442    ) -> impl Future<Output = Result<(), KitError>> + Send + '_;
1443
1444    /// Consumes the implementation, seals its runtime, and settles accepted work.
1445    ///
1446    /// Close cascade-invalidates live descendants after accepted work and
1447    /// interactions settle. Quiescent durable conversations, notifications, and
1448    /// knowledge remain for reopen. This does not terminally close conversations
1449    /// or acknowledge caller events. Drop alone provides no async cleanup guarantee.
1450    ///
1451    /// # Errors
1452    ///
1453    /// Returns [`KitError`] when an owned cleanup responsibility fails or cannot
1454    /// settle within the lifecycle deadline. The consumed handle is not restored;
1455    /// durable recovery owns later reconciliation and supplementary failures do
1456    /// not replace the primary error.
1457    ///
1458    /// See [`Self::new_conversation`] for the complete child lifecycle and [`crate::AgentBackend::new_workspace`] for workspace creation.
1459    fn close(self) -> impl Future<Output = Result<(), KitError>> + Send
1460    where
1461        Self: Sized;
1462}
1463
1464/// Owns one live workspace runtime and its conversation handles.
1465///
1466/// Obtain this unique, non-Clone handle from [`crate::AgentBackend`]. Import
1467/// [`WorkspaceBackend`] for behavior. A committed workspace update invalidates
1468/// prior-revision handles; reopen by typed id instead of expecting in-place
1469/// refresh. Explicit close settles the runtime while preserving quiescent
1470/// durable children for later reopen.
1471///
1472/// See [`WorkspaceBackend`] and [`WorkspaceBackend::new_conversation`] for usage.
1473pub struct Workspace<
1474    S: StateStoreBackend = storage_kit::StateStore,
1475    D: DatabaseBackend = storage_kit::Database,
1476    P: ProviderBackend = providers_kit::Provider,
1477    T: ToolsetBackend = tools_kit::Toolset,
1478> {
1479    inner: WorkspaceInner<S, D, P, T>,
1480}
1481
1482enum WorkspaceInner<S: StateStoreBackend, D: DatabaseBackend, P: ProviderBackend, T: ToolsetBackend>
1483{
1484    Codex(codex::workspace::CodexWorkspace<S, D, T>),
1485    OpenCode(opencode::workspace::OpenCodeWorkspace<S, D>),
1486    Custom(custom::CustomWorkspace<S, D, P, T>),
1487}
1488
1489impl<S, D, P, T> Workspace<S, D, P, T>
1490where
1491    S: StateStoreBackend,
1492    D: DatabaseBackend,
1493    P: ProviderBackend,
1494    T: ToolsetBackend,
1495{
1496    pub(crate) fn codex(backend: codex::workspace::CodexWorkspace<S, D, T>) -> Self {
1497        Self {
1498            inner: WorkspaceInner::Codex(backend),
1499        }
1500    }
1501
1502    pub(crate) fn opencode(backend: opencode::workspace::OpenCodeWorkspace<S, D>) -> Self {
1503        Self {
1504            inner: WorkspaceInner::OpenCode(backend),
1505        }
1506    }
1507
1508    pub(crate) fn custom(backend: custom::CustomWorkspace<S, D, P, T>) -> Self {
1509        Self {
1510            inner: WorkspaceInner::Custom(backend),
1511        }
1512    }
1513}
1514
1515impl<S, D, P, T> WorkspaceBackend for WorkspaceInner<S, D, P, T>
1516where
1517    S: StateStoreBackend,
1518    D: DatabaseBackend,
1519    P: ProviderBackend,
1520    T: ToolsetBackend,
1521{
1522    type Conversation = Conversation<S, D, P, T>;
1523
1524    fn id(&self) -> WorkspaceId {
1525        match self {
1526            WorkspaceInner::Codex(backend) => backend.id(),
1527            WorkspaceInner::OpenCode(backend) => backend.id(),
1528            WorkspaceInner::Custom(backend) => backend.id(),
1529        }
1530    }
1531
1532    async fn info(&self) -> Result<WorkspaceInfo, KitError> {
1533        match self {
1534            WorkspaceInner::Codex(backend) => backend.info().await,
1535            WorkspaceInner::OpenCode(backend) => backend.info().await,
1536            WorkspaceInner::Custom(backend) => backend.info().await,
1537        }
1538    }
1539
1540    async fn new_conversation(
1541        &self,
1542        request: NewConversation,
1543    ) -> Result<Conversation<S, D, P, T>, KitError> {
1544        match self {
1545            WorkspaceInner::Codex(backend) => backend
1546                .new_conversation(request)
1547                .await
1548                .map(Conversation::codex),
1549            WorkspaceInner::OpenCode(backend) => backend
1550                .new_conversation(request)
1551                .await
1552                .map(Conversation::opencode),
1553            WorkspaceInner::Custom(backend) => backend
1554                .new_conversation(request)
1555                .await
1556                .map(Conversation::custom),
1557        }
1558    }
1559
1560    async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
1561        match self {
1562            WorkspaceInner::Codex(backend) => backend.list_conversations().await,
1563            WorkspaceInner::OpenCode(backend) => backend.list_conversations().await,
1564            WorkspaceInner::Custom(backend) => backend.list_conversations().await,
1565        }
1566    }
1567
1568    async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
1569        match self {
1570            WorkspaceInner::Codex(backend) => backend.conversation_info(id).await,
1571            WorkspaceInner::OpenCode(backend) => backend.conversation_info(id).await,
1572            WorkspaceInner::Custom(backend) => backend.conversation_info(id).await,
1573        }
1574    }
1575
1576    async fn get_conversation(
1577        &self,
1578        id: ConversationId,
1579    ) -> Result<Conversation<S, D, P, T>, KitError> {
1580        match self {
1581            WorkspaceInner::Codex(backend) => {
1582                backend.get_conversation(id).await.map(Conversation::codex)
1583            }
1584            WorkspaceInner::OpenCode(backend) => backend
1585                .get_conversation(id)
1586                .await
1587                .map(Conversation::opencode),
1588            WorkspaceInner::Custom(backend) => {
1589                backend.get_conversation(id).await.map(Conversation::custom)
1590            }
1591        }
1592    }
1593
1594    async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
1595        match self {
1596            WorkspaceInner::Codex(backend) => backend.list_tools().await,
1597            WorkspaceInner::OpenCode(backend) => backend.list_tools().await,
1598            WorkspaceInner::Custom(backend) => backend.list_tools().await,
1599        }
1600    }
1601
1602    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
1603        match self {
1604            WorkspaceInner::Codex(backend) => backend.ingest_knowledge(request).await,
1605            WorkspaceInner::OpenCode(backend) => backend.ingest_knowledge(request).await,
1606            WorkspaceInner::Custom(backend) => backend.ingest_knowledge(request).await,
1607        }
1608    }
1609
1610    async fn close(self) -> Result<(), KitError> {
1611        match self {
1612            WorkspaceInner::Codex(backend) => backend.close().await,
1613            WorkspaceInner::OpenCode(backend) => backend.close().await,
1614            WorkspaceInner::Custom(backend) => backend.close().await,
1615        }
1616    }
1617}
1618
1619impl<S, D, P, T> WorkspaceBackend for Workspace<S, D, P, T>
1620where
1621    S: StateStoreBackend,
1622    D: DatabaseBackend,
1623    P: ProviderBackend,
1624    T: ToolsetBackend,
1625{
1626    type Conversation = Conversation<S, D, P, T>;
1627
1628    fn id(&self) -> WorkspaceId {
1629        self.inner.id()
1630    }
1631
1632    async fn info(&self) -> Result<WorkspaceInfo, KitError> {
1633        self.inner.info().await.map_err(|error| {
1634            error
1635                .at_operation(crate::error::Operation::WorkspaceInfo)
1636                .in_phase(crate::error::ErrorPhase::StateRead)
1637        })
1638    }
1639
1640    async fn new_conversation(
1641        &self,
1642        request: NewConversation,
1643    ) -> Result<Conversation<S, D, P, T>, KitError> {
1644        self.inner.new_conversation(request).await.map_err(|error| {
1645            error
1646                .at_operation(crate::error::Operation::NewConversation)
1647                .in_phase(crate::error::ErrorPhase::Admission)
1648        })
1649    }
1650
1651    async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
1652        self.inner.list_conversations().await.map_err(|error| {
1653            error
1654                .at_operation(crate::error::Operation::ListConversations)
1655                .in_phase(crate::error::ErrorPhase::StateRead)
1656        })
1657    }
1658
1659    async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
1660        self.inner.conversation_info(id).await.map_err(|error| {
1661            error
1662                .at_operation(crate::error::Operation::ConversationInfo)
1663                .in_phase(crate::error::ErrorPhase::StateRead)
1664        })
1665    }
1666
1667    async fn get_conversation(
1668        &self,
1669        id: ConversationId,
1670    ) -> Result<Conversation<S, D, P, T>, KitError> {
1671        self.inner.get_conversation(id).await.map_err(|error| {
1672            error
1673                .at_operation(crate::error::Operation::GetConversation)
1674                .in_phase(crate::error::ErrorPhase::Admission)
1675        })
1676    }
1677
1678    async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
1679        self.inner.list_tools().await.map_err(|error| {
1680            error
1681                .at_operation(crate::error::Operation::ListTools)
1682                .in_phase(crate::error::ErrorPhase::InventoryVerification)
1683        })
1684    }
1685
1686    async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
1687        self.inner.ingest_knowledge(request).await.map_err(|error| {
1688            error
1689                .at_operation(crate::error::Operation::IngestKnowledge)
1690                .in_phase(crate::error::ErrorPhase::KnowledgeWrite)
1691        })
1692    }
1693
1694    async fn close(self) -> Result<(), KitError> {
1695        self.inner.close().await.map_err(|error| {
1696            error
1697                .at_operation(crate::error::Operation::CloseWorkspace)
1698                .in_phase(crate::error::ErrorPhase::Lifecycle)
1699        })
1700    }
1701}