use std::ffi::OsString;
use std::fmt;
use std::future::Future;
use std::path::{Path, PathBuf};
use tools_kit::ToolsetBackend;
use providers_kit::ProviderBackend;
use storage_kit::{DatabaseBackend, StateStoreBackend};
use crate::backends::{codex, custom, opencode};
use crate::{
Conversation, ConversationId, ConversationInfo, IngestKnowledgeRequest, KitError,
KnowledgePolicy, ModelSelector, NewConversation, PersonalitySelector, ReasoningEffort,
ToolInfo, WorkspaceId,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WorkspaceRootAccess {
ReadOnly,
ReadWrite,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceRoot {
path: BackendHostPath,
access: WorkspaceRootAccess,
}
#[derive(Clone)]
struct BackendHostPath {
original: PathBuf,
anchor: BackendHostPathAnchor,
components: Vec<BackendHostPathSegment>,
}
impl PartialEq for BackendHostPath {
fn eq(&self, other: &Self) -> bool {
self.anchor == other.anchor && self.components == other.components
}
}
impl Eq for BackendHostPath {}
#[derive(Clone, Debug, Eq, PartialEq)]
enum BackendHostPathAnchor {
Posix,
WindowsDrive(u8),
WindowsUnc {
server: BackendHostPathSegment,
share: BackendHostPathSegment,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum BackendHostPathFlavor {
Posix,
Windows,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct BackendHostPathSegment(Box<[u8]>);
impl fmt::Debug for BackendHostPath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("BackendHostPath")
.field(&self.original)
.finish()
}
}
impl BackendHostPathSegment {
fn new(value: &[u8], label: &str) -> Result<Self, KitError> {
if value.is_empty() || value == b"." || value == b".." {
return Err(KitError::invalid_input(format!(
"{label} must already be lexically normalized"
)));
}
Ok(Self(value.into()))
}
}
impl BackendHostPathAnchor {
fn is_windows(&self) -> bool {
!matches!(self, Self::Posix)
}
fn flavor(&self) -> BackendHostPathFlavor {
if self.is_windows() {
BackendHostPathFlavor::Windows
} else {
BackendHostPathFlavor::Posix
}
}
}
impl BackendHostPath {
fn parse_absolute(label: &str, original: PathBuf) -> Result<Self, KitError> {
let bytes = original.as_os_str().as_encoded_bytes();
if bytes.is_empty() || bytes.contains(&0) {
return Err(KitError::invalid_input(format!(
"{label} must be a nonempty absolute backend-host path"
)));
}
let (anchor, components) = if bytes.starts_with(b"\\\\") {
let mut components = Self::parse_components(&bytes[2..], true, label)?;
if components.len() < 2 || components[0].0.as_ref() == b"?" {
return Err(KitError::invalid_input(format!(
"{label} must use a normalized UNC share path"
)));
}
let share = components.remove(1);
let server = components.remove(0);
(
BackendHostPathAnchor::WindowsUnc { server, share },
components,
)
} else if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\')
{
(
BackendHostPathAnchor::WindowsDrive(bytes[0].to_ascii_lowercase()),
Self::parse_components(&bytes[3..], true, label)?,
)
} else if bytes[0] == b'/' && !bytes.starts_with(b"//") {
(
BackendHostPathAnchor::Posix,
Self::parse_components(&bytes[1..], false, label)?,
)
} else {
return Err(KitError::invalid_input(format!(
"{label} must be a nonempty absolute backend-host path"
)));
};
Ok(Self {
original,
anchor,
components,
})
}
fn parse_components(
bytes: &[u8],
windows: bool,
label: &str,
) -> Result<Vec<BackendHostPathSegment>, KitError> {
if bytes.is_empty() {
return Ok(Vec::new());
}
let mut components = Vec::new();
let mut start = 0;
for (index, byte) in bytes.iter().copied().enumerate() {
if byte == b'/' || (windows && byte == b'\\') {
components.push(BackendHostPathSegment::new(&bytes[start..index], label)?);
start = index + 1;
}
}
components.push(BackendHostPathSegment::new(&bytes[start..], label)?);
Ok(components)
}
fn validate_portable_relative(path: &Path) -> Result<PathBuf, KitError> {
let bytes = path.as_os_str().as_encoded_bytes();
if bytes.is_empty()
|| bytes.contains(&0)
|| matches!(bytes[0], b'/' | b'\\')
|| (bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':')
{
return Err(KitError::invalid_input(
"workspace path must be nonempty and relative",
));
}
if bytes
.split(|byte| matches!(*byte, b'/' | b'\\'))
.any(|component| component.is_empty() || component == b"." || component == b"..")
{
return Err(KitError::invalid_input(
"workspace path must already be lexically normalized",
));
}
Ok(path.to_path_buf())
}
fn resolve(&self, relative: &Path) -> Result<Self, KitError> {
let relative_components = Self::parse_components(
relative.as_os_str().as_encoded_bytes(),
self.anchor.is_windows(),
"workspace path",
)?;
let mut raw = OsString::from(self.original.as_os_str());
let terminated = raw
.as_encoded_bytes()
.last()
.is_some_and(|byte| *byte == b'/' || (self.anchor.is_windows() && *byte == b'\\'));
if !terminated {
raw.push(if self.anchor.is_windows() { "\\" } else { "/" });
}
raw.push(relative.as_os_str());
let resolved = Self::parse_absolute("resolved workspace path", PathBuf::from(raw))?;
let mut expected = self.components.clone();
expected.extend(relative_components);
if resolved.anchor != self.anchor || resolved.components != expected {
return Err(KitError::invalid_input(
"workspace path must remain beneath its backend-host cwd",
));
}
Ok(resolved)
}
fn relative_native_file(&self, native_path: &Path) -> Result<PathBuf, KitError> {
let native = native_path
.to_str()
.filter(|path| !path.is_empty() && !path.contains('\0'))
.ok_or_else(|| KitError::invalid_input("native file path must be nonempty UTF-8"))?;
let windows = self.anchor.is_windows();
let native = if windows {
native.replace('\\', "/")
} else {
native.to_owned()
};
let mut parts = native
.split('/')
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
let root = if windows && native.starts_with("//") {
if parts.len() < 2 {
return Err(KitError::invalid_input("native file path has no UNC share"));
}
let root = Self::parse_absolute(
"native file root",
PathBuf::from(format!(r"\\{}\{}", parts[0], parts[1])),
)?;
parts.drain(..2);
Some(root)
} else if native.as_bytes().get(1) == Some(&b':')
&& native.as_bytes()[0].is_ascii_alphabetic()
{
if !windows || native.as_bytes().get(2) != Some(&b'/') {
return Err(KitError::invalid_input(
"native file path has an incompatible drive",
));
}
let root = Self::parse_absolute("native file root", PathBuf::from(&native[..3]))?;
parts.remove(0);
Some(root)
} else if native.starts_with('/') {
if windows {
return Err(KitError::invalid_input(
"native file path requires an explicit Windows root",
));
}
Some(Self::parse_absolute(
"native file root",
PathBuf::from("/"),
)?)
} else {
if native.starts_with('\\') {
return Err(KitError::invalid_input(
"native file path has an incompatible root",
));
}
None
};
let mut components = match root {
Some(root) if root.anchor == self.anchor => Vec::new(),
Some(_) => {
return Err(KitError::invalid_input(
"native file path has a different root",
));
}
None => self.components.clone(),
};
for part in parts {
match part {
"." => {}
".." => {
if components.pop().is_none() {
return Err(KitError::invalid_input("native file path escapes its root"));
}
}
part => components.push(BackendHostPathSegment::new(
part.as_bytes(),
"native file path",
)?),
}
}
if !components.starts_with(&self.components) || components.len() == self.components.len() {
return Err(KitError::invalid_input(
"native file path does not identify a workspace file",
));
}
let relative = components[self.components.len()..]
.iter()
.map(|part| {
std::str::from_utf8(&part.0)
.map_err(|_| KitError::invalid_input("native file path is not UTF-8"))
})
.collect::<Result<Vec<_>, _>>()?
.join("/");
Self::validate_portable_relative(Path::new(&relative))
}
fn same_flavor(&self, other: &Self) -> bool {
self.anchor.is_windows() == other.anchor.is_windows()
}
fn contains(&self, candidate: &Self) -> bool {
self.anchor == candidate.anchor && candidate.components.starts_with(&self.components)
}
fn overlaps(&self, other: &Self) -> bool {
self.contains(other) || other.contains(self)
}
fn as_path(&self) -> &Path {
&self.original
}
fn into_path_buf(self) -> PathBuf {
self.original
}
}
impl WorkspaceRoot {
pub fn new(path: PathBuf, access: WorkspaceRootAccess) -> Result<Self, KitError> {
Ok(Self {
path: BackendHostPath::parse_absolute("workspace root", path)?,
access,
})
}
pub(crate) fn relative_native_file(&self, path: &Path) -> Result<PathBuf, KitError> {
self.path.relative_native_file(path)
}
#[must_use]
pub fn path(&self) -> &Path {
self.path.as_path()
}
#[must_use]
pub fn access(&self) -> WorkspaceRootAccess {
self.access
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WorkspaceSandboxMode {
ReadOnly,
WorkspaceWrite,
FullAccess,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WorkspaceNetworkAccess {
Disabled,
Enabled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorkspaceExecutionPolicy {
sandbox_mode: WorkspaceSandboxMode,
network_access: WorkspaceNetworkAccess,
}
impl WorkspaceExecutionPolicy {
#[must_use]
pub fn new(sandbox_mode: WorkspaceSandboxMode, network_access: WorkspaceNetworkAccess) -> Self {
Self {
sandbox_mode,
network_access,
}
}
#[must_use]
pub fn sandbox_mode(&self) -> WorkspaceSandboxMode {
self.sandbox_mode
}
#[must_use]
pub fn network_access(&self) -> WorkspaceNetworkAccess {
self.network_access
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceContext {
cwd: BackendHostPath,
roots: Vec<WorkspaceRoot>,
execution_policy: WorkspaceExecutionPolicy,
}
impl WorkspaceContext {
pub fn new(
cwd: PathBuf,
roots: Vec<WorkspaceRoot>,
execution_policy: WorkspaceExecutionPolicy,
) -> Result<Self, KitError> {
let cwd = BackendHostPath::parse_absolute("workspace cwd", cwd)?;
if roots.is_empty() {
return Err(KitError::invalid_input(
"workspace context requires at least one root",
));
}
let mut deduplicated = Vec::<WorkspaceRoot>::new();
for root in roots {
if !cwd.same_flavor(&root.path) {
return Err(KitError::invalid_input(
"workspace cwd and roots must use one backend-host path flavor",
));
}
if let Some(existing) = deduplicated
.iter()
.find(|existing| existing.path == root.path)
{
if existing.access != root.access {
return Err(KitError::invalid_input(
"duplicate workspace roots must not contradict access",
));
}
continue;
}
if deduplicated.iter().any(|existing| {
existing.path.overlaps(&root.path) && existing.access != root.access
}) {
return Err(KitError::invalid_input(
"overlapping workspace roots must not contradict access",
));
}
deduplicated.push(root);
}
let containing = deduplicated
.iter()
.filter(|root| root.path.contains(&cwd))
.collect::<Vec<_>>();
if containing.is_empty() {
return Err(KitError::invalid_input(
"workspace cwd must be contained by a declared root",
));
}
match execution_policy.sandbox_mode {
WorkspaceSandboxMode::ReadOnly
if deduplicated
.iter()
.any(|root| root.access != WorkspaceRootAccess::ReadOnly) =>
{
return Err(KitError::invalid_input(
"read-only workspace policy requires every root to be read-only",
));
}
WorkspaceSandboxMode::WorkspaceWrite
if containing
.iter()
.any(|root| root.access != WorkspaceRootAccess::ReadWrite) =>
{
return Err(KitError::invalid_input(
"workspace-write policy requires the cwd root to be read-write",
));
}
WorkspaceSandboxMode::FullAccess
if deduplicated
.iter()
.any(|root| root.access != WorkspaceRootAccess::ReadWrite) =>
{
return Err(KitError::invalid_input(
"full-access workspace policy requires every root to be read-write",
));
}
_ => {}
}
Ok(Self {
cwd,
roots: deduplicated,
execution_policy,
})
}
pub fn cwd(&self) -> &Path {
self.cwd.as_path()
}
#[must_use]
pub fn roots(&self) -> &[WorkspaceRoot] {
&self.roots
}
#[must_use]
pub fn execution_policy(&self) -> &WorkspaceExecutionPolicy {
&self.execution_policy
}
pub(crate) fn normalized_relative(path: &Path) -> Result<PathBuf, KitError> {
BackendHostPath::validate_portable_relative(path)
}
pub(crate) fn path_flavor(&self) -> BackendHostPathFlavor {
self.cwd.anchor.flavor()
}
pub(crate) fn resolve(&self, path: &Path) -> Result<(PathBuf, PathBuf), KitError> {
let relative = Self::normalized_relative(path)?;
let absolute = self.cwd.resolve(&relative)?;
if !self.roots.iter().any(|root| root.path.contains(&absolute)) {
return Err(KitError::invalid_input(
"workspace path must remain beneath a declared root",
));
}
Ok((relative, absolute.into_path_buf()))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceDefaults {
model: Option<ModelSelector>,
personality: Option<PersonalitySelector>,
reasoning_effort: Option<ReasoningEffort>,
knowledge_policy: Option<KnowledgePolicy>,
}
impl WorkspaceDefaults {
pub fn new(
model: Option<ModelSelector>,
reasoning_effort: Option<ReasoningEffort>,
knowledge_policy: Option<KnowledgePolicy>,
) -> Result<Self, KitError> {
if let Some(policy) = &knowledge_policy {
policy.validate()?;
}
Ok(Self {
model,
personality: None,
reasoning_effort,
knowledge_policy,
})
}
pub fn empty() -> Self {
Self {
model: None,
personality: None,
reasoning_effort: None,
knowledge_policy: None,
}
}
pub fn model(&self) -> Option<&ModelSelector> {
self.model.as_ref()
}
#[must_use]
pub fn with_personality(mut self, personality: PersonalitySelector) -> Self {
self.personality = Some(personality);
self
}
#[must_use]
pub fn personality(&self) -> Option<&PersonalitySelector> {
self.personality.as_ref()
}
pub fn reasoning_effort(&self) -> Option<&ReasoningEffort> {
self.reasoning_effort.as_ref()
}
pub fn knowledge_policy(&self) -> Option<&KnowledgePolicy> {
self.knowledge_policy.as_ref()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NewWorkspace {
name: String,
context: WorkspaceContext,
defaults: WorkspaceDefaults,
}
impl NewWorkspace {
pub fn new(
name: impl Into<String>,
context: WorkspaceContext,
defaults: WorkspaceDefaults,
) -> Result<Self, KitError> {
let name = name.into();
Self::validate_name(&name)?;
Ok(Self {
name,
context,
defaults,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn context(&self) -> &WorkspaceContext {
&self.context
}
pub fn defaults(&self) -> &WorkspaceDefaults {
&self.defaults
}
fn validate_name(name: &str) -> Result<(), KitError> {
if name.is_empty() || name.trim() != name {
return Err(KitError::invalid_input(
"workspace name must be nonempty and unpadded",
));
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceInfo {
id: WorkspaceId,
name: String,
context: WorkspaceContext,
defaults: WorkspaceDefaults,
revision: u64,
}
impl WorkspaceInfo {
pub fn new(
id: WorkspaceId,
name: String,
context: WorkspaceContext,
defaults: WorkspaceDefaults,
revision: u64,
) -> Result<Self, KitError> {
NewWorkspace::validate_name(&name)?;
if revision == 0 {
return Err(KitError::invalid_state(
"workspace revision must be nonzero",
));
}
Ok(Self {
id,
name,
context,
defaults,
revision,
})
}
pub fn id(&self) -> &WorkspaceId {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn context(&self) -> &WorkspaceContext {
&self.context
}
pub fn defaults(&self) -> &WorkspaceDefaults {
&self.defaults
}
pub fn revision(&self) -> u64 {
self.revision
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UpdateWorkspaceRequest {
id: WorkspaceId,
expected_revision: u64,
name: String,
context: WorkspaceContext,
defaults: WorkspaceDefaults,
}
impl UpdateWorkspaceRequest {
pub fn new(
id: WorkspaceId,
expected_revision: u64,
name: impl Into<String>,
context: WorkspaceContext,
defaults: WorkspaceDefaults,
) -> Result<Self, KitError> {
let name = name.into();
NewWorkspace::validate_name(&name)?;
if expected_revision == 0 {
return Err(KitError::invalid_input(
"expected workspace revision must be nonzero",
));
}
Ok(Self {
id,
expected_revision,
name,
context,
defaults,
})
}
pub fn id(&self) -> &WorkspaceId {
&self.id
}
pub fn expected_revision(&self) -> u64 {
self.expected_revision
}
pub fn name(&self) -> &str {
&self.name
}
pub fn context(&self) -> &WorkspaceContext {
&self.context
}
pub fn defaults(&self) -> &WorkspaceDefaults {
&self.defaults
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WorkspaceUpdateReadiness {
Ready,
Busy,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeleteWorkspaceRequest {
id: WorkspaceId,
expected_revision: u64,
}
impl DeleteWorkspaceRequest {
pub fn new(id: WorkspaceId, expected_revision: u64) -> Result<Self, KitError> {
if expected_revision == 0 {
return Err(KitError::invalid_input(
"expected workspace revision must be nonzero",
));
}
Ok(Self {
id,
expected_revision,
})
}
pub fn id(&self) -> &WorkspaceId {
&self.id
}
pub fn expected_revision(&self) -> u64 {
self.expected_revision
}
}
pub trait WorkspaceBackend: Send + Sync + 'static {
type Conversation: crate::ConversationBackend;
fn id(&self) -> WorkspaceId;
fn info(&self) -> impl Future<Output = Result<WorkspaceInfo, KitError>> + Send + '_;
fn new_conversation(
&self,
request: NewConversation,
) -> impl Future<Output = Result<Self::Conversation, KitError>> + Send + '_;
fn list_conversations(
&self,
) -> impl Future<Output = Result<Vec<ConversationInfo>, KitError>> + Send + '_;
fn conversation_info(
&self,
id: ConversationId,
) -> impl Future<Output = Result<ConversationInfo, KitError>> + Send + '_;
fn get_conversation(
&self,
id: ConversationId,
) -> impl Future<Output = Result<Self::Conversation, KitError>> + Send + '_;
fn list_tools(&self) -> impl Future<Output = Result<Vec<ToolInfo>, KitError>> + Send + '_;
fn ingest_knowledge(
&self,
request: IngestKnowledgeRequest,
) -> impl Future<Output = Result<(), KitError>> + Send + '_;
fn close(self) -> impl Future<Output = Result<(), KitError>> + Send
where
Self: Sized;
}
pub struct Workspace<
S: StateStoreBackend = storage_kit::StateStore,
D: DatabaseBackend = storage_kit::Database,
P: ProviderBackend = providers_kit::Provider,
T: ToolsetBackend = tools_kit::Toolset,
> {
inner: WorkspaceInner<S, D, P, T>,
}
enum WorkspaceInner<S: StateStoreBackend, D: DatabaseBackend, P: ProviderBackend, T: ToolsetBackend>
{
Codex(codex::workspace::CodexWorkspace<S, D, T>),
OpenCode(opencode::workspace::OpenCodeWorkspace<S, D>),
Custom(custom::CustomWorkspace<S, D, P, T>),
}
impl<S, D, P, T> Workspace<S, D, P, T>
where
S: StateStoreBackend,
D: DatabaseBackend,
P: ProviderBackend,
T: ToolsetBackend,
{
pub(crate) fn codex(backend: codex::workspace::CodexWorkspace<S, D, T>) -> Self {
Self {
inner: WorkspaceInner::Codex(backend),
}
}
pub(crate) fn opencode(backend: opencode::workspace::OpenCodeWorkspace<S, D>) -> Self {
Self {
inner: WorkspaceInner::OpenCode(backend),
}
}
pub(crate) fn custom(backend: custom::CustomWorkspace<S, D, P, T>) -> Self {
Self {
inner: WorkspaceInner::Custom(backend),
}
}
}
impl<S, D, P, T> WorkspaceBackend for WorkspaceInner<S, D, P, T>
where
S: StateStoreBackend,
D: DatabaseBackend,
P: ProviderBackend,
T: ToolsetBackend,
{
type Conversation = Conversation<S, D, P, T>;
fn id(&self) -> WorkspaceId {
match self {
WorkspaceInner::Codex(backend) => backend.id(),
WorkspaceInner::OpenCode(backend) => backend.id(),
WorkspaceInner::Custom(backend) => backend.id(),
}
}
async fn info(&self) -> Result<WorkspaceInfo, KitError> {
match self {
WorkspaceInner::Codex(backend) => backend.info().await,
WorkspaceInner::OpenCode(backend) => backend.info().await,
WorkspaceInner::Custom(backend) => backend.info().await,
}
}
async fn new_conversation(
&self,
request: NewConversation,
) -> Result<Conversation<S, D, P, T>, KitError> {
match self {
WorkspaceInner::Codex(backend) => backend
.new_conversation(request)
.await
.map(Conversation::codex),
WorkspaceInner::OpenCode(backend) => backend
.new_conversation(request)
.await
.map(Conversation::opencode),
WorkspaceInner::Custom(backend) => backend
.new_conversation(request)
.await
.map(Conversation::custom),
}
}
async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
match self {
WorkspaceInner::Codex(backend) => backend.list_conversations().await,
WorkspaceInner::OpenCode(backend) => backend.list_conversations().await,
WorkspaceInner::Custom(backend) => backend.list_conversations().await,
}
}
async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
match self {
WorkspaceInner::Codex(backend) => backend.conversation_info(id).await,
WorkspaceInner::OpenCode(backend) => backend.conversation_info(id).await,
WorkspaceInner::Custom(backend) => backend.conversation_info(id).await,
}
}
async fn get_conversation(
&self,
id: ConversationId,
) -> Result<Conversation<S, D, P, T>, KitError> {
match self {
WorkspaceInner::Codex(backend) => {
backend.get_conversation(id).await.map(Conversation::codex)
}
WorkspaceInner::OpenCode(backend) => backend
.get_conversation(id)
.await
.map(Conversation::opencode),
WorkspaceInner::Custom(backend) => {
backend.get_conversation(id).await.map(Conversation::custom)
}
}
}
async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
match self {
WorkspaceInner::Codex(backend) => backend.list_tools().await,
WorkspaceInner::OpenCode(backend) => backend.list_tools().await,
WorkspaceInner::Custom(backend) => backend.list_tools().await,
}
}
async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
match self {
WorkspaceInner::Codex(backend) => backend.ingest_knowledge(request).await,
WorkspaceInner::OpenCode(backend) => backend.ingest_knowledge(request).await,
WorkspaceInner::Custom(backend) => backend.ingest_knowledge(request).await,
}
}
async fn close(self) -> Result<(), KitError> {
match self {
WorkspaceInner::Codex(backend) => backend.close().await,
WorkspaceInner::OpenCode(backend) => backend.close().await,
WorkspaceInner::Custom(backend) => backend.close().await,
}
}
}
impl<S, D, P, T> WorkspaceBackend for Workspace<S, D, P, T>
where
S: StateStoreBackend,
D: DatabaseBackend,
P: ProviderBackend,
T: ToolsetBackend,
{
type Conversation = Conversation<S, D, P, T>;
fn id(&self) -> WorkspaceId {
self.inner.id()
}
async fn info(&self) -> Result<WorkspaceInfo, KitError> {
self.inner.info().await.map_err(|error| {
error
.at_operation(crate::error::Operation::WorkspaceInfo)
.in_phase(crate::error::ErrorPhase::StateRead)
})
}
async fn new_conversation(
&self,
request: NewConversation,
) -> Result<Conversation<S, D, P, T>, KitError> {
self.inner.new_conversation(request).await.map_err(|error| {
error
.at_operation(crate::error::Operation::NewConversation)
.in_phase(crate::error::ErrorPhase::Admission)
})
}
async fn list_conversations(&self) -> Result<Vec<ConversationInfo>, KitError> {
self.inner.list_conversations().await.map_err(|error| {
error
.at_operation(crate::error::Operation::ListConversations)
.in_phase(crate::error::ErrorPhase::StateRead)
})
}
async fn conversation_info(&self, id: ConversationId) -> Result<ConversationInfo, KitError> {
self.inner.conversation_info(id).await.map_err(|error| {
error
.at_operation(crate::error::Operation::ConversationInfo)
.in_phase(crate::error::ErrorPhase::StateRead)
})
}
async fn get_conversation(
&self,
id: ConversationId,
) -> Result<Conversation<S, D, P, T>, KitError> {
self.inner.get_conversation(id).await.map_err(|error| {
error
.at_operation(crate::error::Operation::GetConversation)
.in_phase(crate::error::ErrorPhase::Admission)
})
}
async fn list_tools(&self) -> Result<Vec<ToolInfo>, KitError> {
self.inner.list_tools().await.map_err(|error| {
error
.at_operation(crate::error::Operation::ListTools)
.in_phase(crate::error::ErrorPhase::InventoryVerification)
})
}
async fn ingest_knowledge(&self, request: IngestKnowledgeRequest) -> Result<(), KitError> {
self.inner.ingest_knowledge(request).await.map_err(|error| {
error
.at_operation(crate::error::Operation::IngestKnowledge)
.in_phase(crate::error::ErrorPhase::KnowledgeWrite)
})
}
async fn close(self) -> Result<(), KitError> {
self.inner.close().await.map_err(|error| {
error
.at_operation(crate::error::Operation::CloseWorkspace)
.in_phase(crate::error::ErrorPhase::Lifecycle)
})
}
}