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    /// ```
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}