use std::sync::Arc;
use async_trait::async_trait;
use runtime_extension_api::{
CommandOutcome, ExtensionError, InteractionFacade, SubagentFacade, WorkspaceEntry,
WorkspaceFacade,
};
use runtime_ports::{
CommandRequest, InteractionRequest, InteractionSession, OperationContext, PortFailure,
SubagentRequest, SubagentSession, WorkspaceSession,
};
pub(crate) struct PortWorkspaceFacade {
operation: OperationContext,
session: Arc<dyn WorkspaceSession>,
}
impl PortWorkspaceFacade {
pub(crate) fn new(operation: OperationContext, session: Arc<dyn WorkspaceSession>) -> Self {
Self { operation, session }
}
}
#[async_trait]
impl WorkspaceFacade for PortWorkspaceFacade {
async fn describe(&self) -> Result<String, ExtensionError> {
self.session
.describe(&self.operation)
.await
.map_err(map_error)
}
async fn read_text(&self, path: &str) -> Result<String, ExtensionError> {
let bytes = self
.session
.read(&self.operation, path)
.await
.map_err(map_error)?;
String::from_utf8(bytes)
.map_err(|_| ExtensionError::Failed("workspace file is not UTF-8".into()))
}
async fn write_text(&self, path: &str, content: &str) -> Result<(), ExtensionError> {
self.session
.write(&self.operation, path, content.as_bytes())
.await
.map_err(map_error)
}
async fn list(&self, path: &str) -> Result<Vec<WorkspaceEntry>, ExtensionError> {
self.session
.list(&self.operation, path)
.await
.map(|entries| {
entries
.into_iter()
.map(|entry| WorkspaceEntry {
path: entry.path,
is_dir: entry.is_dir,
size: entry.size,
})
.collect()
})
.map_err(map_error)
}
async fn search(
&self,
path: &str,
query: &str,
limit: usize,
) -> Result<Vec<String>, ExtensionError> {
self.session
.search(&self.operation, path, query, limit)
.await
.map_err(map_error)
}
async fn execute(
&self,
command: &str,
cwd: Option<&str>,
) -> Result<CommandOutcome, ExtensionError> {
self.session
.execute(
&self.operation,
CommandRequest {
command: command.to_string(),
cwd: cwd.map(ToOwned::to_owned),
},
)
.await
.map(|output| CommandOutcome {
exit_code: output.exit_code,
stdout: output.stdout,
stderr: output.stderr,
truncated: output.truncated,
})
.map_err(map_error)
}
}
pub(crate) struct PortInteractionFacade {
operation: OperationContext,
request_id: String,
session: Arc<dyn InteractionSession>,
}
impl PortInteractionFacade {
pub(crate) fn new(
operation: OperationContext,
request_id: String,
session: Arc<dyn InteractionSession>,
) -> Self {
Self {
operation,
request_id,
session,
}
}
}
#[async_trait]
impl InteractionFacade for PortInteractionFacade {
async fn request_text(&self, prompt: &str) -> Result<String, ExtensionError> {
let response = self
.session
.request(
&self.operation,
InteractionRequest {
request_id: self.request_id.clone(),
prompt: prompt.to_string(),
},
)
.await
.map_err(map_error)?;
Ok(response.text)
}
}
pub(crate) struct PortSubagentFacade {
operation: OperationContext,
request_id: String,
session: Arc<dyn SubagentSession>,
}
impl PortSubagentFacade {
pub(crate) fn new(
operation: OperationContext,
request_id: String,
session: Arc<dyn SubagentSession>,
) -> Self {
Self {
operation,
request_id,
session,
}
}
}
#[async_trait]
impl SubagentFacade for PortSubagentFacade {
async fn execute(
&self,
prompt: &str,
max_model_turns: usize,
) -> Result<String, ExtensionError> {
self.session
.execute(
&self.operation,
SubagentRequest {
request_id: self.request_id.clone(),
prompt: prompt.to_string(),
max_model_turns,
},
)
.await
.map(|outcome| outcome.answer)
.map_err(map_error)
}
}
fn map_error(error: PortFailure) -> ExtensionError {
use runtime_ports::PortFailureKind;
match error.kind {
PortFailureKind::Invalid => ExtensionError::Invalid(error.message),
PortFailureKind::Forbidden => ExtensionError::Denied(error.message),
PortFailureKind::Canceled => ExtensionError::Canceled,
PortFailureKind::Timeout => ExtensionError::Timeout,
_ => ExtensionError::Failed(format!("{}: {}", error.code, error.message)),
}
}