Clarify model-generated and legacy app path types (#28577)

## Why

`ApiPathString` kind of implies that it can be used anywhere we pull a
path out of JSON, but it's not really appropriate for tool arguments
when the model might generate relative paths.

Prefer `String` for model-generated paths and we can handle the
conversion per feature for now and define a shared abstraction later if
it makes sense.

# What

Rename `ApiPathString` to `AppLegacyPathString` to clarify its role.

Expand the `path-types` skill to tell the model to leave tool args as
bare strings.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-16 20:47:43 +00:00
committed by GitHub
parent a50671e748
commit 322b83de5e
22 changed files with 144 additions and 126 deletions
+20 -17
View File
@@ -30,9 +30,9 @@ use ts_rs::TS;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, TS)]
#[serde(transparent)]
#[ts(type = "string")]
pub struct ApiPathString(String);
pub struct LegacyAppPathString(String);
impl ApiPathString {
impl LegacyAppPathString {
/// Renders an absolute path using the current host's path convention.
pub fn from_abs_path(path: &AbsolutePathBuf) -> Self {
Self(path.to_string_lossy().into_owned())
@@ -48,7 +48,7 @@ impl ApiPathString {
pub fn from_path_uri(
path: &PathUri,
convention: PathConvention,
) -> Result<Self, ApiPathStringError> {
) -> Result<Self, LegacyAppPathStringError> {
if let Some(path_bytes) = path.opaque_fallback_bytes() {
return render_opaque_fallback(path, &path_bytes, convention).map(Self);
}
@@ -61,12 +61,15 @@ impl ApiPathString {
/// Parses this API string as an absolute path using the requested native
/// path convention and returns its canonical path URI.
pub fn to_path_uri(&self, convention: PathConvention) -> Result<PathUri, ApiPathStringError> {
pub fn to_path_uri(
&self,
convention: PathConvention,
) -> Result<PathUri, LegacyAppPathStringError> {
let path = match convention {
PathConvention::Posix => parse_posix_path(&self.0),
PathConvention::Windows => parse_windows_path(&self.0),
};
path.ok_or_else(|| ApiPathStringError::InvalidNativePath {
path.ok_or_else(|| LegacyAppPathStringError::InvalidNativePath {
path: self.0.clone(),
convention,
})
@@ -102,7 +105,7 @@ impl ApiPathString {
}
}
impl From<AbsolutePathBuf> for ApiPathString {
impl From<AbsolutePathBuf> for LegacyAppPathString {
fn from(path: AbsolutePathBuf) -> Self {
Self::from_abs_path(&path)
}
@@ -194,7 +197,7 @@ fn render_opaque_fallback(
path: &PathUri,
path_bytes: &[u8],
convention: PathConvention,
) -> Result<String, ApiPathStringError> {
) -> Result<String, LegacyAppPathStringError> {
let rendered = match convention {
PathConvention::Posix if path_bytes.starts_with(b"/") => {
Some(String::from_utf8_lossy(path_bytes).into_owned())
@@ -202,7 +205,7 @@ fn render_opaque_fallback(
PathConvention::Windows => render_windows_opaque_fallback(path_bytes),
PathConvention::Posix => None,
};
rendered.ok_or_else(|| ApiPathStringError::OpaqueFallback {
rendered.ok_or_else(|| LegacyAppPathStringError::OpaqueFallback {
path: path.to_string(),
})
}
@@ -238,13 +241,13 @@ fn is_windows_separator(character: u16) -> bool {
character == u16::from(b'\\') || character == u16::from(b'/')
}
impl fmt::Display for ApiPathString {
impl fmt::Display for LegacyAppPathString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for ApiPathString {
impl Serialize for LegacyAppPathString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -253,9 +256,9 @@ impl Serialize for ApiPathString {
}
}
impl JsonSchema for ApiPathString {
impl JsonSchema for LegacyAppPathString {
fn schema_name() -> String {
"ApiPathString".to_string()
"LegacyAppPathString".to_string()
}
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
@@ -263,7 +266,7 @@ impl JsonSchema for ApiPathString {
}
}
fn render_posix_path(path: &PathUri) -> Result<String, ApiPathStringError> {
fn render_posix_path(path: &PathUri) -> Result<String, LegacyAppPathStringError> {
let url = path.to_url();
// POSIX file paths do not have a UNC authority, so `file://server/share`
// cannot be represented as `/share` without losing the server identity.
@@ -281,7 +284,7 @@ fn render_posix_path(path: &PathUri) -> Result<String, ApiPathStringError> {
Ok(rendered)
}
fn render_windows_path(path: &PathUri) -> Result<String, ApiPathStringError> {
fn render_windows_path(path: &PathUri) -> Result<String, LegacyAppPathStringError> {
let url = path.to_url();
let mut segments = path_segments(&url);
let mut rendered = String::new();
@@ -342,15 +345,15 @@ fn decode_native_segment(segment: &str) -> String {
String::from_utf8_lossy(&bytes).into_owned()
}
fn incompatible_convention(path: &PathUri, convention: PathConvention) -> ApiPathStringError {
ApiPathStringError::IncompatibleConvention {
fn incompatible_convention(path: &PathUri, convention: PathConvention) -> LegacyAppPathStringError {
LegacyAppPathStringError::IncompatibleConvention {
path: path.to_string(),
convention,
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ApiPathStringError {
pub enum LegacyAppPathStringError {
#[error("opaque fallback path URI `{path}` cannot be recovered as a native path")]
OpaqueFallback { path: String },
#[error("path URI `{path}` cannot be rendered using {convention} path syntax")]