first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
agent::action_result::{AnyFileContent, FileContext},
|
||||
skills::{ParsedSkill, SkillProvider, SkillScope},
|
||||
};
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use thiserror::Error;
|
||||
use warp_multi_agent_api as api;
|
||||
use warp_util::host_id::HostId;
|
||||
use warp_util::local_or_remote_path::LocalOrRemotePath;
|
||||
use warp_util::remote_path::RemotePath;
|
||||
use warp_util::standardized_path::StandardizedPath;
|
||||
|
||||
use crate::agent::action_result::{AnyFileContent, FileContext};
|
||||
use crate::skills::{ParsedSkill, SkillProvider, SkillReference, SkillScope};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SkillConversionError {
|
||||
@@ -22,14 +24,126 @@ pub enum SkillConversionError {
|
||||
ProviderInvalid,
|
||||
#[error("Invalid content")]
|
||||
ContentInvalid,
|
||||
#[error("Skill path origin is unavailable")]
|
||||
PathOriginUnavailable,
|
||||
#[error("Invalid remote skill path")]
|
||||
RemotePathInvalid,
|
||||
}
|
||||
/// Identifies how a string skill path from an API payload should be interpreted.
|
||||
///
|
||||
/// Live agent responses can be decoded from the active session's location. Restored payloads do
|
||||
/// not carry enough session identity to safely reconstruct path-based skill locations, so callers
|
||||
/// must use [`SkillPathOrigin::Unavailable`] rather than silently assuming the local filesystem.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum SkillPathOrigin {
|
||||
Local,
|
||||
Remote {
|
||||
host_id: HostId,
|
||||
},
|
||||
/// Path identity could not be restored, but the API payload already carries the skill
|
||||
/// descriptor and content needed to render a historical transcript.
|
||||
///
|
||||
/// This intentionally uses a local path wrapper only as a display-compatible identity for
|
||||
/// restored conversation UI. Live execution paths should use [`SkillPathOrigin::Local`] or
|
||||
/// [`SkillPathOrigin::Remote`] so local/remote provenance is preserved.
|
||||
RestoredDisplayOnly,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
impl SkillPathOrigin {
|
||||
pub fn location_for_path(
|
||||
&self,
|
||||
path: impl Into<String>,
|
||||
) -> Result<LocalOrRemotePath, SkillConversionError> {
|
||||
let path = path.into();
|
||||
match self {
|
||||
SkillPathOrigin::Local | SkillPathOrigin::RestoredDisplayOnly => {
|
||||
// Normalize the path to collapse duplicate separators (e.g. `//workspace/...`
|
||||
// → `/workspace/...`) so skill cache lookups match the filesystem-derived keys.
|
||||
// We operate on the raw string rather than using `PathBuf::components().collect()`
|
||||
// because the latter re-serialises with platform-specific separators (backslashes
|
||||
// on Windows) and treats leading `//` as a UNC prefix on Windows.
|
||||
let normalized = collapse_slashes(&path);
|
||||
Ok(LocalOrRemotePath::Local(PathBuf::from(normalized)))
|
||||
}
|
||||
SkillPathOrigin::Remote { host_id } => {
|
||||
let path = StandardizedPath::try_new(&path)
|
||||
.map_err(|_| SkillConversionError::RemotePathInvalid)?;
|
||||
Ok(LocalOrRemotePath::Remote(RemotePath::new(
|
||||
host_id.clone(),
|
||||
path,
|
||||
)))
|
||||
}
|
||||
SkillPathOrigin::Unavailable => Err(SkillConversionError::PathOriginUnavailable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collapse consecutive `/` separators into a single one.
|
||||
///
|
||||
/// Skill paths are always forward-slash POSIX-style paths on all platforms, so we normalise
|
||||
/// at the string level rather than using [`std::path::PathBuf::components`], which would
|
||||
/// re-serialise with backslashes on Windows and misinterpret `//prefix` as a UNC path.
|
||||
fn collapse_slashes(path: &str) -> String {
|
||||
let mut result = String::with_capacity(path.len());
|
||||
let mut prev_slash = false;
|
||||
for ch in path.chars() {
|
||||
if ch == '/' {
|
||||
if !prev_slash {
|
||||
result.push(ch);
|
||||
}
|
||||
prev_slash = true;
|
||||
} else {
|
||||
result.push(ch);
|
||||
prev_slash = false;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn skill_reference_for_path(
|
||||
path: impl Into<String>,
|
||||
path_origin: &SkillPathOrigin,
|
||||
) -> Result<SkillReference, SkillConversionError> {
|
||||
path_origin
|
||||
.location_for_path(path)
|
||||
.map(SkillReference::Path)
|
||||
}
|
||||
|
||||
pub fn skill_reference_from_api_skill_ref(
|
||||
skill_ref: api::SkillRef,
|
||||
path_origin: &SkillPathOrigin,
|
||||
) -> Option<SkillReference> {
|
||||
match skill_ref.skill_reference {
|
||||
Some(api::skill_ref::SkillReference::Path(path)) => {
|
||||
skill_reference_for_path(path, path_origin).ok()
|
||||
}
|
||||
Some(api::skill_ref::SkillReference::BundledSkillId(id)) => {
|
||||
Some(SkillReference::BundledSkillId(id))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn skill_reference_from_read_skill_ref(
|
||||
skill_reference: api::message::tool_call::read_skill::SkillReference,
|
||||
path_origin: &SkillPathOrigin,
|
||||
) -> Result<SkillReference, SkillConversionError> {
|
||||
match skill_reference {
|
||||
api::message::tool_call::read_skill::SkillReference::SkillPath(path) => {
|
||||
skill_reference_for_path(path, path_origin)
|
||||
}
|
||||
api::message::tool_call::read_skill::SkillReference::BundledSkillId(id) => {
|
||||
Ok(SkillReference::BundledSkillId(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<ParsedSkill> for api::Skill {
|
||||
fn from(skill: ParsedSkill) -> Self {
|
||||
api::Skill {
|
||||
descriptor: Some(api::SkillDescriptor {
|
||||
skill_reference: Some(api::skill_descriptor::SkillReference::Path(
|
||||
skill.path.to_string_lossy().to_string(),
|
||||
skill.path.display_path(),
|
||||
)),
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
@@ -37,7 +151,7 @@ impl From<ParsedSkill> for api::Skill {
|
||||
provider: Some(skill.provider.into()),
|
||||
}),
|
||||
content: Some(api::FileContent {
|
||||
file_path: skill.path.to_string_lossy().to_string(),
|
||||
file_path: skill.path.display_path(),
|
||||
content: skill.content,
|
||||
line_range: skill
|
||||
.line_range
|
||||
@@ -89,6 +203,15 @@ impl TryFrom<api::Skill> for ParsedSkill {
|
||||
type Error = SkillConversionError;
|
||||
|
||||
fn try_from(api_skill: api::Skill) -> Result<Self, Self::Error> {
|
||||
Self::try_from_api_with_origin(api_skill, &SkillPathOrigin::Unavailable)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParsedSkill {
|
||||
pub fn try_from_api_with_origin(
|
||||
api_skill: api::Skill,
|
||||
path_origin: &SkillPathOrigin,
|
||||
) -> Result<Self, SkillConversionError> {
|
||||
let Some(descriptor) = api_skill.descriptor else {
|
||||
return Err(SkillConversionError::MissingDescriptor);
|
||||
};
|
||||
@@ -121,7 +244,7 @@ impl TryFrom<api::Skill> for ParsedSkill {
|
||||
let line_range = context.line_range.as_ref();
|
||||
|
||||
Ok(ParsedSkill {
|
||||
path: PathBuf::from(&path),
|
||||
path: path_origin.location_for_path(path)?,
|
||||
name: descriptor.name,
|
||||
description: descriptor.description,
|
||||
content,
|
||||
@@ -164,3 +287,7 @@ fn convert_provider(
|
||||
api::skill_descriptor::provider::Type::OpenCode(_) => Ok(SkillProvider::OpenCode),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "conversion_tests.rs"]
|
||||
mod conversion_tests;
|
||||
|
||||
Reference in New Issue
Block a user