diff --git a/Cargo.lock b/Cargo.lock index 11f2fcd44..d265c74b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,7 @@ version = "1.2.0" dependencies = [ "anyhow", "derive_more", + "diffy", "schemars 1.2.1", "serde", "serde_json", @@ -175,6 +176,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diffy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05264ab2aab4fb952fc4b0f3f6eff1ddfb4563064053a4ea174d91537584a769" +dependencies = [ + "hashbrown 0.17.0", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -193,6 +203,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "hashbrown" version = "0.12.3" @@ -204,6 +220,9 @@ name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "foldhash", +] [[package]] name = "heck" diff --git a/agent-client-protocol-schema/Cargo.toml b/agent-client-protocol-schema/Cargo.toml index a9b6e78e6..16e204b09 100644 --- a/agent-client-protocol-schema/Cargo.toml +++ b/agent-client-protocol-schema/Cargo.toml @@ -36,7 +36,7 @@ unstable = [ # Protocol v2 is intentionally NOT part of the `unstable` umbrella. # It introduces a parallel `v2` module and (eventually) a different wire # version, so it must be opted into explicitly. -unstable_protocol_v2 = [] +unstable_protocol_v2 = ["dep:diffy"] unstable_auth_methods = [] unstable_elicitation = [] unstable_llm_providers = [] @@ -55,6 +55,7 @@ tracing = ["dep:tracing"] [dependencies] anyhow = "1" derive_more = { version = "2", features = ["from", "display"] } +diffy = { version = "0.5.0", optional = true } schemars = { version = "1" } # `rc` is required for Arc-based protocol strings and RawValue payloads. serde = { version = "1", features = ["derive", "rc"] } diff --git a/agent-client-protocol-schema/src/v2/conversion.rs b/agent-client-protocol-schema/src/v2/conversion.rs index 58832dc51..12af41fb7 100644 --- a/agent-client-protocol-schema/src/v2/conversion.rs +++ b/agent-client-protocol-schema/src/v2/conversion.rs @@ -8,7 +8,7 @@ use std::{ collections::{BTreeMap, HashMap}, fmt, hash::{BuildHasher, Hash}, - path::PathBuf, + path::{Path, PathBuf}, sync::Arc, }; @@ -2693,18 +2693,9 @@ impl IntoV1 for super::Diff { type Output = crate::v1::Diff; fn into_v1(self) -> Result { - let Self { - path, - old_text, - new_text, - meta, - } = self; - Ok(crate::v1::Diff { - path: path.into_v1()?, - old_text: old_text.into_v1()?, - new_text: new_text.into_v1()?, - meta: meta.into_v1()?, - }) + Err(ProtocolConversionError::new( + "v2 Diff cannot be represented in v1 because v1 requires oldText/newText while v2 carries standard patch text and structured changes", + )) } } @@ -2718,13 +2709,40 @@ impl IntoV2 for crate::v1::Diff { new_text, meta, } = self; - Ok(super::Diff { - path: path.into_v2()?, - old_text: old_text.into_v2()?, - new_text: new_text.into_v2()?, - meta: meta.into_v2()?, - }) + let path = path.into_v2()?; + let old_text = old_text.into_v2()?; + let new_text = new_text.into_v2()?; + let change = if old_text.is_some() { + super::DiffChange::modify(path.clone()).file_type(super::DiffFileType::Text) + } else { + super::DiffChange::add(path.clone()).file_type(super::DiffFileType::Text) + }; + let diff = full_file_git_patch(&path, old_text.as_deref(), &new_text); + + Ok(super::Diff::patch(diff, vec![change]).meta(meta.into_v2()?)) + } +} + +fn full_file_git_patch(path: &Path, old_text: Option<&str>, new_text: &str) -> String { + let path = path.to_string_lossy(); + let old = old_text.unwrap_or_default(); + let original_filename = if old_text.is_some() { + path.to_string() + } else { + "/dev/null".to_string() + }; + + let mut options = diffy::DiffOptions::new(); + options + .set_original_filename(original_filename) + .set_modified_filename(path.to_string()); + + let mut diff = format!("diff --git {path} {path}\n"); + if old_text.is_none() { + diff.push_str("new file mode 100644\n"); } + diff.push_str(&options.create_patch(old, new_text).to_string()); + diff } impl IntoV1 for super::ToolCallLocation { @@ -9765,9 +9783,17 @@ mod tests { "content": [ { "type": "diff", - "path": "/path", - "oldText": "old contents", - "newText": "new contents" + "changes": [ + { + "operation": "modify", + "path": "/path", + "fileType": "text" + } + ], + "patch": { + "format": "git_patch", + "diff": "diff --git /path /path\n--- /path\n+++ /path\n@@ -1 +1 @@\n-old contents\n\\ No newline at end of file\n+new contents\n\\ No newline at end of file\n" + } } ], "locations": [ @@ -9790,7 +9816,7 @@ mod tests { assert_eq!(back.fields.title.as_deref(), Some("editing files")); assert_eq!(back.fields.kind, Some(v1::ToolKind::Edit)); assert_eq!(back.fields.status, Some(v1::ToolCallStatus::InProgress)); - assert_eq!(back.fields.content.as_ref().map(Vec::len), Some(1)); + assert_eq!(back.fields.content.as_ref().map(Vec::len), Some(0)); assert_eq!(back.fields.locations.as_ref().map(Vec::len), Some(1)); assert_eq!( back.fields.raw_input, @@ -10252,20 +10278,17 @@ mod tests { "_chart", BTreeMap::default(), )), - v2::ToolCallContent::Diff(v2::Diff::new("/tmp/file.txt", "new")), + v2::ToolCallContent::Diff(v2::Diff::patch( + "diff --git /tmp/file.txt /tmp/file.txt\n", + vec![v2::DiffChange::modify("/tmp/file.txt")], + )), ]); let converted: v1::ToolCallUpdate = v2_to_v1(update).unwrap(); assert_eq!(converted.fields.kind, None); assert_eq!(converted.fields.status, None); - assert_eq!( - converted.fields.content, - Some(vec![v1::ToolCallContent::Diff(v1::Diff::new( - "/tmp/file.txt", - "new" - ))]) - ); + assert_eq!(converted.fields.content, Some(vec![])); } #[test] @@ -10414,9 +10437,9 @@ mod tests { let converted: v2::ToolCallUpdate = v1_to_v2(update).unwrap(); assert_eq!( converted.content, - crate::MaybeUndefined::Value(vec![v2::ToolCallContent::Diff(v2::Diff::new( - "/tmp/file.txt", - "new" + crate::MaybeUndefined::Value(vec![v2::ToolCallContent::Diff(v2::Diff::patch( + full_file_git_patch(&PathBuf::from("/tmp/file.txt"), None, "new"), + vec![v2::DiffChange::add("/tmp/file.txt").file_type(v2::DiffFileType::Text)], ))]) ); } diff --git a/agent-client-protocol-schema/src/v2/tool_call.rs b/agent-client-protocol-schema/src/v2/tool_call.rs index 463cf30ba..9a382e990 100644 --- a/agent-client-protocol-schema/src/v2/tool_call.rs +++ b/agent-client-protocol-schema/src/v2/tool_call.rs @@ -484,9 +484,12 @@ impl Content { } } -/// A diff representing file modifications. +/// File changes produced by a tool call. /// -/// Shows changes to files in a format suitable for display in the client UI. +/// `changes` identifies affected absolute paths and operations. `patch` +/// optionally carries renderable patch text for clients that can show it. +/// Agents SHOULD provide `patch` whenever feasible. Clients MUST handle diffs +/// where `patch` is omitted or `null`. /// /// See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content) #[serde_as] @@ -495,15 +498,20 @@ impl Content { #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct Diff { - /// The absolute file path being modified. - pub path: PathBuf, - /// The original content (None for new files). + /// Structured file changes described by this diff. + /// + /// Clients can use this field without parsing patch text to determine affected paths. + #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")] + #[schemars(extend("x-deserialize-skip-invalid-items" = true))] + pub changes: Vec, + /// Renderable patch text. + /// + /// Agents SHOULD provide patch text whenever feasible. Omitted or `null` + /// means no renderable patch text was provided. #[serde_as(deserialize_as = "DefaultOnError")] #[schemars(extend("x-deserialize-default-on-error" = true))] #[serde(default)] - pub old_text: Option, - /// The new content after modification. - pub new_text: String, + pub patch: Option, /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -517,21 +525,26 @@ pub struct Diff { } impl Diff { - /// Builds [`Diff`] with the required fields set; optional fields start unset or empty. + /// Builds [`Diff`] with structured file changes. #[must_use] - pub fn new(path: impl Into, new_text: impl Into) -> Self { + pub fn new(changes: Vec) -> Self { Self { - path: path.into(), - old_text: None, - new_text: new_text.into(), + changes, + patch: None, meta: None, } } - /// The original content (None for new files). + /// Builds [`Diff`] with git patch text and structured file changes. + #[must_use] + pub fn patch(diff: impl Into, changes: Vec) -> Self { + Self::new(changes).with_patch(DiffPatch::new(diff)) + } + + /// Sets renderable patch text. #[must_use] - pub fn old_text(mut self, old_text: impl IntoOption) -> Self { - self.old_text = old_text.into_option(); + pub fn with_patch(mut self, patch: impl IntoOption) -> Self { + self.patch = patch.into_option(); self } @@ -547,6 +560,319 @@ impl Diff { } } +/// Renderable patch text. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DiffPatch { + /// Patch format. The only ACP-defined value is `git_patch`. + pub format: DiffPatchFormat, + /// Git patch text. + pub diff: String, +} + +impl DiffPatch { + /// Builds [`DiffPatch`] with git patch text. + #[must_use] + pub fn new(diff: impl Into) -> Self { + Self { + format: DiffPatchFormat::GitPatch, + diff: diff.into(), + } + } +} + +/// Text patch format used by [`DiffPatch`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum DiffPatchFormat { + /// Git patch format. + GitPatch, + /// Custom or future diff format. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + #[serde(untagged)] + Other(String), +} + +/// Kind of file content represented by a diff change. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum DiffFileType { + /// Text content. + Text, + /// Binary or otherwise non-text content. + Binary, + /// Directory entry. + Directory, + /// Symbolic link. + Symlink, + /// Custom or future file type. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + #[serde(untagged)] + Other(String), +} + +/// One file-level change described by a [`Diff`]. +/// +/// Structured change metadata lets clients identify affected files and +/// operations without parsing the text patch. +#[serde_as] +#[skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DiffChange { + /// File content kind. + /// + /// Omitted or `null` means the content kind is unknown. + #[serde_as(deserialize_as = "DefaultOnError")] + #[schemars(extend("x-deserialize-default-on-error" = true))] + #[serde(default)] + pub file_type: Option, + /// MIME type of the file contents. + /// + /// Omitted or `null` means the MIME type is unknown. + #[serde_as(deserialize_as = "DefaultOnError")] + #[schemars(extend("x-deserialize-default-on-error" = true))] + #[serde(default)] + pub mime_type: Option, + /// File operation-specific fields. + #[serde(flatten)] + pub operation: DiffChangeOperation, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[serde_as(deserialize_as = "DefaultOnError")] + #[schemars(extend("x-deserialize-default-on-error" = true))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl DiffChange { + /// Builds [`DiffChange`] with the required fields set; optional fields start unset or empty. + #[must_use] + pub fn new(operation: DiffChangeOperation) -> Self { + Self { + file_type: None, + mime_type: None, + operation, + meta: None, + } + } + + /// Builds a file add change. + #[must_use] + pub fn add(path: impl Into) -> Self { + Self::new(DiffChangeOperation::Add(DiffPathChange::new(path))) + } + + /// Builds a file delete change. + #[must_use] + pub fn delete(path: impl Into) -> Self { + Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path))) + } + + /// Builds a file modify change. + #[must_use] + pub fn modify(path: impl Into) -> Self { + Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path))) + } + + /// Builds a file move or rename change. + #[must_use] + pub fn move_file(old_path: impl Into, path: impl Into) -> Self { + Self::new(DiffChangeOperation::Move(DiffPathPairChange::new( + old_path, path, + ))) + } + + /// Builds a file copy change. + #[must_use] + pub fn copy(old_path: impl Into, path: impl Into) -> Self { + Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new( + old_path, path, + ))) + } + + /// File content kind. + /// + /// Omitted or `null` means the content kind is unknown. + #[must_use] + pub fn file_type(mut self, file_type: impl IntoOption) -> Self { + self.file_type = file_type.into_option(); + self + } + + /// MIME type of the file contents. + /// + /// Omitted or `null` means the MIME type is unknown. + #[must_use] + pub fn mime_type(mut self, mime_type: impl IntoOption) -> Self { + self.mime_type = mime_type.into_option(); + self + } + + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// File operation for a [`DiffChange`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "operation", rename_all = "snake_case")] +#[schemars(extend("discriminator" = {"propertyName": "operation"}))] +#[non_exhaustive] +pub enum DiffChangeOperation { + /// A file was added. + Add(DiffPathChange), + /// A file was deleted. + Delete(DiffPathChange), + /// A file was modified in place. + Modify(DiffPathChange), + /// A file was moved or renamed. + Move(DiffPathPairChange), + /// A file was copied. + Copy(DiffPathPairChange), + /// Custom or future file operation. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + #[serde(untagged)] + Other(OtherDiffChange), +} + +/// Operation metadata for add, delete, and modify changes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DiffPathChange { + /// Absolute path for the operation. + pub path: PathBuf, +} + +impl DiffPathChange { + /// Builds [`DiffPathChange`] with the required fields set; optional fields start unset or empty. + #[must_use] + pub fn new(path: impl Into) -> Self { + Self { path: path.into() } + } +} + +/// Operation metadata for move and copy changes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DiffPathPairChange { + /// Absolute path before the operation. + pub old_path: PathBuf, + /// Absolute path after the operation. + pub path: PathBuf, +} + +impl DiffPathPairChange { + /// Builds [`DiffPathPairChange`] with the required fields set; optional fields start unset or empty. + #[must_use] + pub fn new(old_path: impl Into, path: impl Into) -> Self { + Self { + old_path: old_path.into(), + path: path.into(), + } + } +} + +/// Custom or future file operation payload. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[schemars(inline)] +#[schemars(transform = other_diff_change_schema)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct OtherDiffChange { + /// Custom or future file operation. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + pub operation: String, + /// Additional fields from the unknown file operation payload. + #[serde(flatten)] + pub fields: BTreeMap, +} + +impl OtherDiffChange { + /// Builds [`OtherDiffChange`] from an unknown discriminator and preserves the remaining extension fields. + #[must_use] + pub fn new( + operation: impl Into, + mut fields: BTreeMap, + ) -> Self { + fields.remove("operation"); + fields.remove("fileType"); + fields.remove("mimeType"); + fields.remove("_meta"); + Self { + operation: operation.into(), + fields, + } + } +} + +impl<'de> Deserialize<'de> for OtherDiffChange { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut fields = BTreeMap::::deserialize(deserializer)?; + let operation = fields + .remove("operation") + .ok_or_else(|| serde::de::Error::missing_field("operation"))?; + let serde_json::Value::String(operation) = operation else { + return Err(serde::de::Error::custom("`operation` must be a string")); + }; + + if is_known_diff_change_operation(&operation) { + return Err(serde::de::Error::custom(format!( + "known diff change operation `{operation}` did not match its schema" + ))); + } + fields.remove("fileType"); + fields.remove("mimeType"); + fields.remove("_meta"); + + Ok(Self { operation, fields }) + } +} + +fn is_known_diff_change_operation(operation: &str) -> bool { + matches!(operation, "add" | "delete" | "modify" | "move" | "copy") +} + +fn other_diff_change_schema(schema: &mut Schema) { + super::schema_util::reject_known_string_discriminators( + schema, + "operation", + &["add", "delete", "modify", "move", "copy"], + ); +} + /// A file location being accessed or modified by a tool. /// /// Enables clients to implement "follow-along" features that track @@ -759,6 +1085,113 @@ mod tests { ); } + #[test] + fn diff_patch_serializes_git_patch_with_structured_changes() { + let diff = ToolCallContent::Diff(Diff::patch( + "diff --git /repo/config.json /repo/config.json\n", + vec![ + DiffChange::modify("/repo/config.json") + .file_type(DiffFileType::Text) + .mime_type("application/json"), + ], + )); + + assert_eq!( + serde_json::to_value(diff).unwrap(), + serde_json::json!({ + "type": "diff", + "changes": [ + { + "operation": "modify", + "path": "/repo/config.json", + "fileType": "text", + "mimeType": "application/json" + } + ], + "patch": { + "format": "git_patch", + "diff": "diff --git /repo/config.json /repo/config.json\n" + } + }) + ); + } + + #[test] + fn diff_serializes_binary_modify_without_patch_text() { + let diff = ToolCallContent::Diff(Diff::new(vec![ + DiffChange::modify("/repo/assets/logo.png") + .file_type(DiffFileType::Binary) + .mime_type("image/png"), + ])); + + assert_eq!( + serde_json::to_value(diff).unwrap(), + serde_json::json!({ + "type": "diff", + "changes": [ + { + "operation": "modify", + "path": "/repo/assets/logo.png", + "fileType": "binary", + "mimeType": "image/png" + } + ] + }) + ); + } + + #[test] + fn diff_move_serializes_shared_fields_with_operation_payload() { + let diff = ToolCallContent::Diff(Diff::new(vec![ + DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs") + .file_type(DiffFileType::Text) + .mime_type("text/rust"), + ])); + + assert_eq!( + serde_json::to_value(diff).unwrap(), + serde_json::json!({ + "type": "diff", + "changes": [ + { + "operation": "move", + "oldPath": "/repo/src/old.rs", + "path": "/repo/src/new.rs", + "fileType": "text", + "mimeType": "text/rust" + } + ] + }) + ); + } + + #[test] + fn diff_changes_skip_malformed_list_items() { + let content: ToolCallContent = serde_json::from_value(serde_json::json!({ + "type": "diff", + "changes": [ + { + "operation": "modify" + }, + { + "operation": "delete", + "path": "/ok" + } + ], + "patch": { + "format": "git_patch", + "diff": "diff --git /ok /ok\n" + } + })) + .unwrap(); + + let ToolCallContent::Diff(diff) = content else { + panic!("expected diff content"); + }; + assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]); + assert_eq!(diff.patch, Some(DiffPatch::new("diff --git /ok /ok\n"))); + } + #[test] fn tool_kind_preserves_unknown_variant() { let kind: ToolKind = serde_json::from_str("\"review\"").unwrap(); diff --git a/docs/docs.json b/docs/docs.json index 4bb4cd662..4dbbb7b51 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -209,6 +209,7 @@ "rfds/v2/client-filesystem-terminal-capabilities", "rfds/v2/plan-variants", "rfds/v2/tool-call-updates", + "rfds/v2/diff-file-states", "rfds/v2/permission-requests", "rfds/v2/message-updates" ] diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index c07113cc5..a4a13ef03 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -3267,9 +3267,12 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d ## Diff -A diff representing file modifications. +File changes produced by a tool call. -Shows changes to files in a format suitable for display in the client UI. +`changes` identifies affected absolute paths and operations. `patch` +optionally carries renderable patch text for clients that can show it. +Agents SHOULD provide `patch` whenever feasible. Clients MUST handle diffs +where `patch` is omitted or `null`. See protocol docs: [Content](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#content) @@ -3285,14 +3288,250 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - The new content after modification. +DiffChange[]} required> + Structured file changes described by this diff. + +Clients can use this field without parsing patch text to determine affected paths. + + +DiffPatch | null} > + Renderable patch text. + +Agents SHOULD provide patch text whenever feasible. Omitted or `null` +means no renderable patch text was provided. + + + +## DiffChange + +One file-level change described by a `Diff`. + +Structured change metadata lets clients identify affected files and +operations without parsing the text patch. + +**Type:** Union + +**Shared properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + + +DiffFileType | null} > + File content kind. + +Omitted or `null` means the content kind is unknown. + + + + MIME type of the file contents. + +Omitted or `null` means the MIME type is unknown. + + + +**Variants:** + + +A file was added. + + + + + The discriminator value. Must be `"add"`. + + + Absolute path for the operation. + + + + + + +A file was deleted. + + + + + The discriminator value. Must be `"delete"`. - - The original content (None for new files). + + Absolute path for the operation. + + + + + + +A file was modified in place. + + + + + The discriminator value. Must be `"modify"`. - The absolute file path being modified. + Absolute path for the operation. + + + + + + +A file was moved or renamed. + + + + + Absolute path before the operation. + + + The discriminator value. Must be `"move"`. + + + Absolute path after the operation. + + + + + + +A file was copied. + + + + + Absolute path before the operation. + + + The discriminator value. Must be `"copy"`. + + + Absolute path after the operation. + + + + + + +Custom or future file operation. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + + + Custom or future file operation. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + + + + +## DiffFileType + +Kind of file content represented by a diff change. + +**Type:** Union + + + Text content. + + + + Binary or otherwise non-text content. + + + + Directory entry. + + + + Symbolic link. + + + +Custom or future file type. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + +## DiffPatch + +Renderable patch text. + +**Type:** Object + +**Properties:** + + + Git patch text. + +DiffPatchFormat} + required +> + Patch format. The only ACP-defined value is `git_patch`. + + +## DiffPatchFormat + +Text patch format used by `DiffPatch`. + +**Type:** Union + + + Git patch format. + + + +Custom or future diff format. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + +## DiffPathChange + +Operation metadata for add, delete, and modify changes. + +**Type:** Object + +**Properties:** + + + Absolute path for the operation. + + +## DiffPathPairChange + +Operation metadata for move and copy changes. + +**Type:** Object + +**Properties:** + + + Absolute path before the operation. + + + Absolute path after the operation. ## ElicitationAcceptAction @@ -8294,14 +8533,18 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - The new content after modification. - - - The original content (None for new files). +DiffChange[]} required> + Structured file changes described by this diff. + +Clients can use this field without parsing patch text to determine affected paths. + - - The absolute file path being modified. +DiffPatch | null} > + Renderable patch text. + +Agents SHOULD provide patch text whenever feasible. Omitted or `null` +means no renderable patch text was provided. + The discriminator value. Must be `"diff"`. diff --git a/docs/protocol/v2/draft/tool-calls.mdx b/docs/protocol/v2/draft/tool-calls.mdx index 5a01b3397..b15733a08 100644 --- a/docs/protocol/v2/draft/tool-calls.mdx +++ b/docs/protocol/v2/draft/tool-calls.mdx @@ -332,29 +332,86 @@ Standard [content blocks](/protocol/v2/draft/content) like text, images, or reso ### Diffs -File modifications shown as diffs: +File modifications shown as diffs. A diff always includes structured file +changes and can optionally include renderable patch text. + +Clients can use `changes` to identify affected absolute paths and file +operations without parsing patch text. When `patch` is present, clients can +also render the patch text directly. Agents SHOULD provide `patch` whenever +feasible. Clients MUST handle diffs where `patch` is omitted or `null`. ```json { "type": "diff", - "path": "/home/user/project/src/config.json", - "oldText": "{\n \"debug\": false\n}", - "newText": "{\n \"debug\": true\n}" + "changes": [ + { + "operation": "modify", + "path": "/home/user/project/src/config.json", + "fileType": "text", + "mimeType": "application/json" + } + ], + "patch": { + "format": "git_patch", + "diff": "diff --git /home/user/project/src/config.json /home/user/project/src/config.json\n--- /home/user/project/src/config.json\n+++ /home/user/project/src/config.json\n@@ -1,3 +1,3 @@\n {\n- \"debug\": false\n+ \"debug\": true\n }\n" + } } ``` - - The absolute file path being modified + + Structured file changes described by this diff. + + + + Optional renderable patch text. Agents SHOULD provide this whenever feasible. + Omitted or `null` means no patch text was provided. + + + + Patch format. The ACP-defined value is `git_patch`. + + + + Git patch text. + + + + File operation: `add`, `delete`, `modify`, `move`, or `copy`. - - The original content (null for new files) + + The absolute path after the operation. For deletes, this is the deleted path. - - The new content after modification + + The absolute path before the operation. Required for `move` and `copy`. + + Optional file kind: `text`, `binary`, `directory`, or `symlink`. + + + + Optional MIME type for the file contents. + + +Omit `patch` when there is no useful text patch, such as a same-path binary +update or symlink target change: + +```json +{ + "type": "diff", + "changes": [ + { + "operation": "modify", + "path": "/home/user/project/assets/logo.png", + "fileType": "binary", + "mimeType": "image/png" + } + ] +} +``` + ## Following the Agent Tool calls can report file locations they're working with, enabling Clients to implement "follow-along" features that track which files the Agent is accessing or modifying in real-time. diff --git a/docs/protocol/v2/schema.mdx b/docs/protocol/v2/schema.mdx index ed546af5b..74bc67c91 100644 --- a/docs/protocol/v2/schema.mdx +++ b/docs/protocol/v2/schema.mdx @@ -1672,9 +1672,12 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/e ## Diff -A diff representing file modifications. +File changes produced by a tool call. -Shows changes to files in a format suitable for display in the client UI. +`changes` identifies affected absolute paths and operations. `patch` +optionally carries renderable patch text for clients that can show it. +Agents SHOULD provide `patch` whenever feasible. Clients MUST handle diffs +where `patch` is omitted or `null`. See protocol docs: [Content](https://agentclientprotocol.com/protocol/v2/tool-calls#content) @@ -1690,14 +1693,250 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility) - - The new content after modification. +DiffChange[]} required> + Structured file changes described by this diff. + +Clients can use this field without parsing patch text to determine affected paths. + + +DiffPatch | null} > + Renderable patch text. + +Agents SHOULD provide patch text whenever feasible. Omitted or `null` +means no renderable patch text was provided. + + + +## DiffChange + +One file-level change described by a `Diff`. + +Structured change metadata lets clients identify affected files and +operations without parsing the text patch. + +**Type:** Union + +**Shared properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility) + + +DiffFileType | null} > + File content kind. + +Omitted or `null` means the content kind is unknown. + + + + MIME type of the file contents. + +Omitted or `null` means the MIME type is unknown. + + + +**Variants:** + + +A file was added. + + + + + The discriminator value. Must be `"add"`. + + + Absolute path for the operation. + + + + + + +A file was deleted. + + + + + The discriminator value. Must be `"delete"`. + + + Absolute path for the operation. + + + + + + +A file was modified in place. + + + + + The discriminator value. Must be `"modify"`. + + + Absolute path for the operation. + + + + + + +A file was moved or renamed. + + + + + Absolute path before the operation. + + + The discriminator value. Must be `"move"`. + + + Absolute path after the operation. + + + + + + +A file was copied. + + + + + Absolute path before the operation. + + + The discriminator value. Must be `"copy"`. + + + Absolute path after the operation. + + + + + + +Custom or future file operation. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + + + Custom or future file operation. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + + + + +## DiffFileType + +Kind of file content represented by a diff change. + +**Type:** Union + + + Text content. + + + + Binary or otherwise non-text content. + + + + Directory entry. + + + + Symbolic link. + + + +Custom or future file type. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + +## DiffPatch + +Renderable patch text. + +**Type:** Object + +**Properties:** + + + Git patch text. + +DiffPatchFormat} + required +> + Patch format. The only ACP-defined value is `git_patch`. + + +## DiffPatchFormat + +Text patch format used by `DiffPatch`. + +**Type:** Union + + + Git patch format. + + + +Custom or future diff format. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + +## DiffPathChange + +Operation metadata for add, delete, and modify changes. + +**Type:** Object + +**Properties:** + + + Absolute path for the operation. - - The original content (None for new files). + +## DiffPathPairChange + +Operation metadata for move and copy changes. + +**Type:** Object + +**Properties:** + + + Absolute path before the operation. - The absolute file path being modified. + Absolute path after the operation. ## EmbeddedResource @@ -4079,14 +4318,18 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility) - - The new content after modification. - - - The original content (None for new files). +DiffChange[]} required> + Structured file changes described by this diff. + +Clients can use this field without parsing patch text to determine affected paths. + - - The absolute file path being modified. +DiffPatch | null} > + Renderable patch text. + +Agents SHOULD provide patch text whenever feasible. Omitted or `null` +means no renderable patch text was provided. + The discriminator value. Must be `"diff"`. diff --git a/docs/protocol/v2/tool-calls.mdx b/docs/protocol/v2/tool-calls.mdx index 6c18533e1..6ed0205c1 100644 --- a/docs/protocol/v2/tool-calls.mdx +++ b/docs/protocol/v2/tool-calls.mdx @@ -332,29 +332,86 @@ Standard [content blocks](/protocol/v2/content) like text, images, or resources: ### Diffs -File modifications shown as diffs: +File modifications shown as diffs. A diff always includes structured file +changes and can optionally include renderable patch text. + +Clients can use `changes` to identify affected absolute paths and file +operations without parsing patch text. When `patch` is present, clients can +also render the patch text directly. Agents SHOULD provide `patch` whenever +feasible. Clients MUST handle diffs where `patch` is omitted or `null`. ```json { "type": "diff", - "path": "/home/user/project/src/config.json", - "oldText": "{\n \"debug\": false\n}", - "newText": "{\n \"debug\": true\n}" + "changes": [ + { + "operation": "modify", + "path": "/home/user/project/src/config.json", + "fileType": "text", + "mimeType": "application/json" + } + ], + "patch": { + "format": "git_patch", + "diff": "diff --git /home/user/project/src/config.json /home/user/project/src/config.json\n--- /home/user/project/src/config.json\n+++ /home/user/project/src/config.json\n@@ -1,3 +1,3 @@\n {\n- \"debug\": false\n+ \"debug\": true\n }\n" + } } ``` - - The absolute file path being modified + + Structured file changes described by this diff. + + + + Optional renderable patch text. Agents SHOULD provide this whenever feasible. + Omitted or `null` means no patch text was provided. + + + + Patch format. The ACP-defined value is `git_patch`. + + + + Git patch text. + + + + File operation: `add`, `delete`, `modify`, `move`, or `copy`. - - The original content (null for new files) + + The absolute path after the operation. For deletes, this is the deleted path. - - The new content after modification + + The absolute path before the operation. Required for `move` and `copy`. + + Optional file kind: `text`, `binary`, `directory`, or `symlink`. + + + + Optional MIME type for the file contents. + + +Omit `patch` when there is no useful text patch, such as a same-path binary +update or symlink target change: + +```json +{ + "type": "diff", + "changes": [ + { + "operation": "modify", + "path": "/home/user/project/assets/logo.png", + "fileType": "binary", + "mimeType": "image/png" + } + ] +} +``` + ## Following the Agent Tool calls can report file locations they're working with, enabling Clients to implement "follow-along" features that track which files the Agent is accessing or modifying in real-time. diff --git a/docs/rfds/v2/diff-file-states.mdx b/docs/rfds/v2/diff-file-states.mdx new file mode 100644 index 000000000..a8677c160 --- /dev/null +++ b/docs/rfds/v2/diff-file-states.mdx @@ -0,0 +1,189 @@ +--- +title: "v2 Diff File States" +--- + +Author(s): [@benbrandt](https://github.com/benbrandt) + +## Elevator pitch + +> What are you proposing to change? + +ACP v2 should replace the v1 `Diff` shape's ambiguous `oldText` / `newText` +fields with a structured diff content shape that can represent text patches, +non-text file changes, and file-level operations such as add, delete, modify, +move, and copy. + +Every diff should carry structured file-change metadata. Text changes can also +carry renderable git patch text in an optional top-level `patch` field. + +## Status quo + +> How do things work today and what problems does this cause? Why would we change things? + +ACP v1 represents a diff as a path plus old and new text: + +```json +{ + "type": "diff", + "path": "/home/user/project/src/config.json", + "oldText": "{\n \"debug\": false\n}", + "newText": "{\n \"debug\": true\n}" +} +``` + +That shape has several problems: + +- `newText: ""` cannot distinguish a deleted file from an empty file without an extra flag. +- Moves and copies have no direct representation. +- Binary files, symlinks, and directory entries do not have useful `oldText` and `newText` values. +- Clients have to reconstruct or parse a diff to render changes. +- The protocol cannot express whether a file path was added, deleted, modified, moved, or copied. + +The v1 [Represent deleted files in diff](/rfds/diff-delete) RFD is a useful +stop-gap for deletes, but v2 can use a cleaner breaking shape instead of adding +more flags to the v1 structure. + +## What we propose to do about it + +> What are you proposing to improve the situation? + +ACP v2 should make `ToolCallContent` diffs carry a required `changes` array and +an optional `patch` object. + +Use `patch` for changes that have useful renderable patch text: + +```json +{ + "type": "diff", + "changes": [ + { + "operation": "modify", + "path": "/home/user/project/src/config.json", + "fileType": "text", + "mimeType": "application/json" + } + ], + "patch": { + "format": "git_patch", + "diff": "diff --git /home/user/project/src/config.json /home/user/project/src/config.json\n--- /home/user/project/src/config.json\n+++ /home/user/project/src/config.json\n@@ -1,3 +1,3 @@\n {\n- \"debug\": false\n+ \"debug\": true\n }\n" + } +} +``` + +The patch format should be `git_patch`. A single git patch can describe several +files, so the patch is top-level context for the whole `changes` array. Clients +can render the patch text directly for baseline compatibility, and can use +`changes` to identify affected paths and operations without parsing the patch. +Agents SHOULD provide `patch` whenever feasible. Clients MUST handle diffs +where `patch` is omitted or `null`. + +Omit `patch` when patch text is not useful: + +```json +{ + "type": "diff", + "changes": [ + { + "operation": "modify", + "path": "/home/user/project/assets/logo.png", + "fileType": "binary", + "mimeType": "image/png" + } + ] +} +``` + +Omitted and `null` `patch` values are equivalent and mean no renderable patch +text was provided. + +The initial operation set should be: + +- `add` +- `delete` +- `modify` +- `move` +- `copy` + +`add`, `delete`, and `modify` use `path`. `move` and `copy` use `oldPath` and +`path`. All paths in protocol payloads are absolute. + +The optional `fileType` field should describe the file entry when known: + +- `text` +- `binary` +- `directory` +- `symlink` + +The optional `mimeType` field should carry a MIME type when known. Omitted and +`null` values for `fileType` and `mimeType` mean the value is unknown. + +## Shiny future + +> How will things play out once this feature exists? + +Clients can render text diffs without needing to synthesize patch text, and can +show file-level summaries without parsing patch syntax. Agents can report binary +updates, symlink changes, deletes, moves, and copies without forcing those cases +through text-only fields. + +The v2 diff surface is also easier for SDKs to model because operations are +explicit enum variants rather than inferred states from optional text fields. + +## Implementation details and plan + +> Tell me more about your implementation. What is your detailed implementation plan? + +1. Replace the v2 `Diff` struct's old text fields with a required `changes` array. +2. Add an optional `patch` field with `format` and `diff`. +3. Use `git_patch` as the ACP-defined patch format, and document that Agents SHOULD provide it whenever feasible. +4. Add structured `DiffChange` operation variants for add, delete, modify, move, and copy, with path fields owned by each operation variant. +5. Add optional `fileType` and `mimeType` fields for non-text and unknown file content. +6. Preserve custom or future operations where the receiver can safely store, replay, forward, or display a generic summary. +7. Update generated schema and v2 protocol docs. + +## Frequently asked questions + +> What questions have arisen over the course of authoring this document or during subsequent discussions? + +### Why use git patch instead of unified diff? + +Git patch is broadly supported, renderable as text, and can carry file-level +headers for multiple files. ACP can still add future patch formats if another +format becomes necessary, but v2 should define one default format first. + +### Why is `patch` top-level instead of attached to individual changes? + +Git patch text can describe several files and several operation kinds at once. +Keeping `patch` top-level makes it renderable context for the whole diff content +item, while `changes` remains the structured source for paths and operations. + +### Should patch text be restricted to some operation variants? + +No. Git patches can represent adds, deletes, modifies, moves/renames, copies, +and some binary changes. If an Agent has useful patch text for a set of changes, +it can include `patch`; otherwise it can omit it. + +### Why is `patch` optional if Agents SHOULD provide it? + +Some changes have no useful patch text, especially same-path binary updates and +symlink target changes. Patch generation can also fail or be unavailable in a +given Agent implementation. Keeping `patch` optional lets clients rely on +structured `changes`, while the SHOULD encourages better rendering when patch +text is practical. + +### What kind of `modify` does not have a patch? + +Mostly in-place non-text updates, such as a binary file update at the same path +or a symlink target change. Text modifications should normally use +`patch` so clients have renderable text. + +### Why include both patch text and structured changes? + +Patch text gives clients an immediate baseline rendering path. Structured +changes give clients operation and path metadata without requiring patch +parsing, which matters for summaries, file trees, permissions, and non-text +indicators. + +## Revision history + +- 2026-07-02: Initial draft diff --git a/docs/rfds/v2/overview.mdx b/docs/rfds/v2/overview.mdx index 116766096..a4a9f7d82 100644 --- a/docs/rfds/v2/overview.mdx +++ b/docs/rfds/v2/overview.mdx @@ -34,6 +34,7 @@ Current RFDs with active maintainer focus that are targeting the v2 release - [Client Filesystem and Terminal Surface](./client-filesystem-terminal-capabilities.mdx) - [Plan Variants](./plan-variants.mdx) - [Tool Call Updates](./tool-call-updates.mdx) +- [Diff File States](./diff-file-states.mdx) - [Permission Requests](./permission-requests.mdx) - [Message Updates and Chunks](./message-updates.mdx) - [Remote Transports](../streamable-http-websocket-transport.mdx) @@ -48,6 +49,7 @@ Other RFDs will progress separately and are not dependent on breaking changes (s - Make [plan variants](./plan-variants.mdx) the default v2 plan shape by replacing the old `plan` session update with item-based `plan_update`. - Replace the v1 split between `tool_call` and `tool_call_update` with a single [tool-call update](./tool-call-updates.mdx) upsert shape keyed by `toolCallId`. - Add [tool-call content chunks](./tool-call-updates.mdx) so Agents can stream individual `ToolCallContent` items that append to a tool call. +- Replace the v1 `Diff` `oldText` / `newText` shape with [diff file states](./diff-file-states.mdx): renderable `git_patch` text for text changes plus structured file operations for add, delete, modify, move, copy, and non-text changes. - Make [permission requests](./permission-requests.mdx) carry a required prompt `title` and optional extensible `subject` tagged union. Tool-call permissions use `subject.type: "tool_call"` with the same `ToolCallUpdate` payload shape as session updates, while subject-less permissions rely on the common prompt fields. - Add [whole-message updates](./message-updates.mdx) for `user_message`, `agent_message`, and `agent_thought` alongside streamed chunks. Message updates are upserts keyed by `messageId`; their `content` arrays replace current message content, while chunks append to the current content. - Require [message IDs](../message-id.mdx) on streamed message chunks. @@ -74,7 +76,6 @@ Changes under consideration that still need to be drafted or moved to draft: - Streaming/Non-streaming consistency follow-ups: - Terminal Output type for streaming terminal output from an agent - - Expand diff types (delete, move) - MCP: tool timeouts, more lifecycle methods ## Shiny future @@ -107,6 +108,7 @@ With the needed breaking changes, as much as possible I am targeting having a co ## Revision history +- 2026-07-02: Added the v2 Diff File States RFD for renderable git patches, structured file operations, and non-text file changes. - 2026-07-02: Moved the v2 RFD collection to Active. - 2026-07-02: Added the v2 Permission Requests RFD for required permission titles, optional structured subjects, and tool-call permission subjects. - 2026-07-02: Added the v2 Required Session Methods RFD and recorded that `session/load`, `session/list`, `session/resume`, and `session/close` are baseline when `session` is present. diff --git a/schema/v2/schema.json b/schema/v2/schema.json index 74a61c8c3..e3e26f7e2 100644 --- a/schema/v2/schema.json +++ b/schema/v2/schema.json @@ -1454,22 +1454,300 @@ }, "required": ["content"] }, - "Diff": { - "description": "A diff representing file modifications.\n\nShows changes to files in a format suitable for display in the client UI.\n\nSee protocol docs: [Content](https://agentclientprotocol.com/protocol/v2/tool-calls#content)", + "DiffChange": { + "description": "One file-level change described by a [`Diff`].\n\nStructured change metadata lets clients identify affected files and\noperations without parsing the text patch.", "type": "object", "properties": { - "path": { - "description": "The absolute file path being modified.", - "type": "string" + "fileType": { + "description": "File content kind.\n\nOmitted or `null` means the content kind is unknown.", + "anyOf": [ + { + "$ref": "#/$defs/DiffFileType" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true }, - "oldText": { - "description": "The original content (None for new files).", + "mimeType": { + "description": "MIME type of the file contents.\n\nOmitted or `null` means the MIME type is unknown.", "type": ["string", "null"], "x-deserialize-default-on-error": true }, - "newText": { - "description": "The new content after modification.", + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "anyOf": [ + { + "description": "A file was added.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "add" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathChange" + } + ] + }, + { + "description": "A file was deleted.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "delete" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathChange" + } + ] + }, + { + "description": "A file was modified in place.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "modify" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathChange" + } + ] + }, + { + "description": "A file was moved or renamed.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "move" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathPairChange" + } + ] + }, + { + "description": "A file was copied.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "copy" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathPairChange" + } + ] + }, + { + "title": "other", + "description": "Custom or future file operation.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "object", + "properties": { + "operation": { + "description": "Custom or future file operation.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + }, + "required": ["operation"], + "not": { + "anyOf": [ + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "add" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "delete" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "modify" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "move" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "copy" + } + }, + "required": ["operation"] + } + ] + }, + "additionalProperties": true + } + ], + "discriminator": { + "propertyName": "operation" + } + }, + "DiffFileType": { + "description": "Kind of file content represented by a diff change.", + "anyOf": [ + { + "description": "Text content.", + "type": "string", + "const": "text" + }, + { + "description": "Binary or otherwise non-text content.", + "type": "string", + "const": "binary" + }, + { + "description": "Directory entry.", + "type": "string", + "const": "directory" + }, + { + "description": "Symbolic link.", + "type": "string", + "const": "symlink" + }, + { + "title": "other", + "description": "Custom or future file type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + ] + }, + "DiffPathChange": { + "description": "Operation metadata for add, delete, and modify changes.", + "type": "object", + "properties": { + "path": { + "description": "Absolute path for the operation.", "type": "string" + } + }, + "required": ["path"] + }, + "DiffPathPairChange": { + "description": "Operation metadata for move and copy changes.", + "type": "object", + "properties": { + "oldPath": { + "description": "Absolute path before the operation.", + "type": "string" + }, + "path": { + "description": "Absolute path after the operation.", + "type": "string" + } + }, + "required": ["oldPath", "path"] + }, + "DiffPatch": { + "description": "Renderable patch text.", + "type": "object", + "properties": { + "format": { + "description": "Patch format. The only ACP-defined value is `git_patch`.", + "allOf": [ + { + "$ref": "#/$defs/DiffPatchFormat" + } + ] + }, + "diff": { + "description": "Git patch text.", + "type": "string" + } + }, + "required": ["format", "diff"] + }, + "DiffPatchFormat": { + "description": "Text patch format used by [`DiffPatch`].", + "anyOf": [ + { + "description": "Git patch format.", + "type": "string", + "const": "git_patch" + }, + { + "title": "other", + "description": "Custom or future diff format.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + ] + }, + "Diff": { + "description": "File changes produced by a tool call.\n\n`changes` identifies affected absolute paths and operations. `patch`\noptionally carries renderable patch text for clients that can show it.\nAgents SHOULD provide `patch` whenever feasible. Clients MUST handle diffs\nwhere `patch` is omitted or `null`.\n\nSee protocol docs: [Content](https://agentclientprotocol.com/protocol/v2/tool-calls#content)", + "type": "object", + "properties": { + "changes": { + "description": "Structured file changes described by this diff.\n\nClients can use this field without parsing patch text to determine affected paths.", + "type": "array", + "items": { + "$ref": "#/$defs/DiffChange" + }, + "x-deserialize-skip-invalid-items": true + }, + "patch": { + "description": "Renderable patch text.\n\nAgents SHOULD provide patch text whenever feasible. Omitted or `null`\nmeans no renderable patch text was provided.", + "anyOf": [ + { + "$ref": "#/$defs/DiffPatch" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true }, "_meta": { "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)", @@ -1478,7 +1756,7 @@ "additionalProperties": true } }, - "required": ["path", "newText"] + "required": ["changes"] }, "ToolCallLocation": { "description": "A file location being accessed or modified by a tool.\n\nEnables clients to implement \"follow-along\" features that track\nwhich files the agent is working with in real-time.\n\nSee protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/v2/tool-calls#following-the-agent)", diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 2ba4f2832..846c68e67 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -1598,22 +1598,300 @@ }, "required": ["content"] }, - "Diff": { - "description": "A diff representing file modifications.\n\nShows changes to files in a format suitable for display in the client UI.\n\nSee protocol docs: [Content](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#content)", + "DiffChange": { + "description": "One file-level change described by a [`Diff`].\n\nStructured change metadata lets clients identify affected files and\noperations without parsing the text patch.", "type": "object", "properties": { - "path": { - "description": "The absolute file path being modified.", - "type": "string" + "fileType": { + "description": "File content kind.\n\nOmitted or `null` means the content kind is unknown.", + "anyOf": [ + { + "$ref": "#/$defs/DiffFileType" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true }, - "oldText": { - "description": "The original content (None for new files).", + "mimeType": { + "description": "MIME type of the file contents.\n\nOmitted or `null` means the MIME type is unknown.", "type": ["string", "null"], "x-deserialize-default-on-error": true }, - "newText": { - "description": "The new content after modification.", + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "anyOf": [ + { + "description": "A file was added.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "add" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathChange" + } + ] + }, + { + "description": "A file was deleted.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "delete" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathChange" + } + ] + }, + { + "description": "A file was modified in place.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "modify" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathChange" + } + ] + }, + { + "description": "A file was moved or renamed.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "move" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathPairChange" + } + ] + }, + { + "description": "A file was copied.", + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "copy" + } + }, + "required": ["operation"], + "allOf": [ + { + "$ref": "#/$defs/DiffPathPairChange" + } + ] + }, + { + "title": "other", + "description": "Custom or future file operation.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "object", + "properties": { + "operation": { + "description": "Custom or future file operation.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + }, + "required": ["operation"], + "not": { + "anyOf": [ + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "add" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "delete" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "modify" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "move" + } + }, + "required": ["operation"] + }, + { + "type": "object", + "properties": { + "operation": { + "type": "string", + "const": "copy" + } + }, + "required": ["operation"] + } + ] + }, + "additionalProperties": true + } + ], + "discriminator": { + "propertyName": "operation" + } + }, + "DiffFileType": { + "description": "Kind of file content represented by a diff change.", + "anyOf": [ + { + "description": "Text content.", + "type": "string", + "const": "text" + }, + { + "description": "Binary or otherwise non-text content.", + "type": "string", + "const": "binary" + }, + { + "description": "Directory entry.", + "type": "string", + "const": "directory" + }, + { + "description": "Symbolic link.", + "type": "string", + "const": "symlink" + }, + { + "title": "other", + "description": "Custom or future file type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + ] + }, + "DiffPathChange": { + "description": "Operation metadata for add, delete, and modify changes.", + "type": "object", + "properties": { + "path": { + "description": "Absolute path for the operation.", "type": "string" + } + }, + "required": ["path"] + }, + "DiffPathPairChange": { + "description": "Operation metadata for move and copy changes.", + "type": "object", + "properties": { + "oldPath": { + "description": "Absolute path before the operation.", + "type": "string" + }, + "path": { + "description": "Absolute path after the operation.", + "type": "string" + } + }, + "required": ["oldPath", "path"] + }, + "DiffPatch": { + "description": "Renderable patch text.", + "type": "object", + "properties": { + "format": { + "description": "Patch format. The only ACP-defined value is `git_patch`.", + "allOf": [ + { + "$ref": "#/$defs/DiffPatchFormat" + } + ] + }, + "diff": { + "description": "Git patch text.", + "type": "string" + } + }, + "required": ["format", "diff"] + }, + "DiffPatchFormat": { + "description": "Text patch format used by [`DiffPatch`].", + "anyOf": [ + { + "description": "Git patch format.", + "type": "string", + "const": "git_patch" + }, + { + "title": "other", + "description": "Custom or future diff format.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + ] + }, + "Diff": { + "description": "File changes produced by a tool call.\n\n`changes` identifies affected absolute paths and operations. `patch`\noptionally carries renderable patch text for clients that can show it.\nAgents SHOULD provide `patch` whenever feasible. Clients MUST handle diffs\nwhere `patch` is omitted or `null`.\n\nSee protocol docs: [Content](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#content)", + "type": "object", + "properties": { + "changes": { + "description": "Structured file changes described by this diff.\n\nClients can use this field without parsing patch text to determine affected paths.", + "type": "array", + "items": { + "$ref": "#/$defs/DiffChange" + }, + "x-deserialize-skip-invalid-items": true + }, + "patch": { + "description": "Renderable patch text.\n\nAgents SHOULD provide patch text whenever feasible. Omitted or `null`\nmeans no renderable patch text was provided.", + "anyOf": [ + { + "$ref": "#/$defs/DiffPatch" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true }, "_meta": { "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", @@ -1622,7 +1900,7 @@ "additionalProperties": true } }, - "required": ["path", "newText"] + "required": ["changes"] }, "ToolCallLocation": { "description": "A file location being accessed or modified by a tool.\n\nEnables clients to implement \"follow-along\" features that track\nwhich files the agent is working with in real-time.\n\nSee protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#following-the-agent)",