Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 77 additions & 42 deletions agent-client-protocol-schema/src/v2/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,14 @@ pub struct SessionMessage {
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
/// Omitted leaves metadata unchanged; `null` clears it.
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
/// unchanged, a key set to `null` is removed, and any other value replaces that key's
/// stored value without merging nested objects. Beyond this patch rule, implementations
/// MUST NOT make assumptions about values at these keys.
///
/// Implementations MUST NOT make assumptions about values in `_meta`.
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
Expand Down Expand Up @@ -350,7 +355,10 @@ impl SessionMessage {
self
}

/// Sets, clears, or leaves unchanged message metadata.
/// Patches, clears, or leaves unchanged message metadata.
///
/// A concrete object patches stored metadata by top-level key; see
/// [`apply_meta_update`](super::apply_meta_update).
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
Expand Down Expand Up @@ -726,7 +734,8 @@ pub enum CompactionStatus {
///
/// `summary`, `error`, and `_meta` have patch semantics: omission leaves the
/// stored value unchanged, `null` clears it, and a concrete value replaces it.
/// `summary: []` also clears the summary.
/// `summary: []` also clears the summary. A concrete `_meta` object patches the
/// stored metadata by top-level key instead of replacing it.
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
Expand All @@ -748,7 +757,14 @@ pub struct CompactionUpdate {
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub error: MaybeUndefined<String>,
/// Extensible metadata patch for this compaction.
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
/// unchanged, a key set to `null` is removed, and any other value replaces that key's
/// stored value without merging nested objects. Beyond this patch rule, implementations
/// MUST NOT make assumptions about values at these keys.
///
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
Expand Down Expand Up @@ -786,7 +802,10 @@ impl CompactionUpdate {
self
}

/// Sets, clears, or omits the metadata patch.
/// Patches, clears, or leaves unchanged compaction metadata.
///
/// A concrete object patches stored metadata by top-level key; see
/// [`apply_meta_update`](super::apply_meta_update).
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
Expand Down Expand Up @@ -852,9 +871,10 @@ impl CompactionSummaryChunk {
///
/// Only [`SubagentUpdate::session_id`] is required. Other fields have
/// patch semantics: omitted fields leave the stored value unchanged, `null`
/// clears or unsets the value, and concrete values replace it. For `state`,
/// `null` removes the current state report and leaves activity unconfirmed;
/// it does not report idle or cancellation. A child whose capabilities are
/// clears or unsets the value, and concrete values replace it. A concrete
/// `_meta` object instead patches the stored metadata by top-level key. For
/// `state`, `null` removes the current state report and leaves activity
/// unconfirmed; it does not report idle or cancellation. A child whose capabilities are
/// unset permits no Client-initiated session mutations.
///
/// The title and description provide the parent's display metadata for the
Expand Down Expand Up @@ -910,10 +930,13 @@ pub struct SubagentUpdate {
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub state: MaybeUndefined<StateUpdate>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Omitted means no metadata update; `null` is an
/// explicit clear signal. Implementations MUST NOT make assumptions about values at these keys.
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
/// unchanged, a key set to `null` is removed, and any other value replaces that key's
/// stored value without merging nested objects. Beyond this patch rule, implementations
/// MUST NOT make assumptions about values at these keys.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
Expand Down Expand Up @@ -970,7 +993,10 @@ impl SubagentUpdate {
self
}

/// Sets, clears, or leaves unchanged subagent metadata.
/// Patches, clears, or leaves unchanged subagent metadata.
///
/// A concrete object patches stored metadata by top-level key; see
/// [`apply_meta_update`](super::apply_meta_update).
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
Expand Down Expand Up @@ -1278,7 +1304,8 @@ impl ConfigOptionUpdate {
/// This allows clients to display dynamic session names and track session state changes.
///
/// Omitted fields leave the existing session info unchanged. `null` clears the
/// corresponding value.
/// corresponding value. A concrete `_meta` object patches the stored metadata by
/// top-level key.
#[serde_as]
#[skip_serializing_none]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
Expand All @@ -1297,10 +1324,13 @@ pub struct SessionInfoUpdate {
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub updated_at: MaybeUndefined<String>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Omitted means no metadata update; `null` is an
/// explicit clear signal. Implementations MUST NOT make assumptions about values at these keys.
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
/// unchanged, a key set to `null` is removed, and any other value replaces that key's
/// stored value without merging nested objects. Beyond this patch rule, implementations
/// MUST NOT make assumptions about values at these keys.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
Expand Down Expand Up @@ -1332,11 +1362,10 @@ impl SessionInfoUpdate {
self
}

/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Omitted means no metadata update; `null` is an
/// explicit clear signal. Implementations MUST NOT make assumptions about values at these keys.
/// Patches, clears, or leaves unchanged session metadata.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// A concrete object patches stored metadata by top-level key; see
/// [`apply_meta_update`](super::apply_meta_update).
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
Expand Down Expand Up @@ -1933,10 +1962,13 @@ pub struct UserMessage {
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
/// 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. Omitted means no metadata update; `null` is an explicit clear signal.
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
/// unchanged, a key set to `null` is removed, and any other value replaces that key's
/// stored value without merging nested objects. Beyond this patch rule, implementations
/// MUST NOT make assumptions about values at these keys.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
Expand Down Expand Up @@ -1965,11 +1997,10 @@ impl UserMessage {
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.
/// Patches, clears, or leaves unchanged message metadata.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// A concrete object patches stored metadata by top-level key; see
/// [`apply_meta_update`](super::apply_meta_update).
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
Expand Down Expand Up @@ -2005,10 +2036,13 @@ pub struct AgentMessage {
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
/// 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. Omitted means no metadata update; `null` is an explicit clear signal.
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
/// unchanged, a key set to `null` is removed, and any other value replaces that key's
/// stored value without merging nested objects. Beyond this patch rule, implementations
/// MUST NOT make assumptions about values at these keys.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
Expand Down Expand Up @@ -2037,11 +2071,10 @@ impl AgentMessage {
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.
/// Patches, clears, or leaves unchanged message metadata.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// A concrete object patches stored metadata by top-level key; see
/// [`apply_meta_update`](super::apply_meta_update).
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
Expand Down Expand Up @@ -2077,10 +2110,13 @@ pub struct AgentThought {
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
pub content: MaybeUndefined<Vec<ContentBlock>>,
/// 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. Omitted means no metadata update; `null` is an explicit clear signal.
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
/// unchanged, a key set to `null` is removed, and any other value replaces that key's
/// stored value without merging nested objects. Beyond this patch rule, implementations
/// MUST NOT make assumptions about values at these keys.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
#[serde(
Expand Down Expand Up @@ -2109,11 +2145,10 @@ impl AgentThought {
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.
/// Patches, clears, or leaves unchanged thought metadata.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
/// A concrete object patches stored metadata by top-level key; see
/// [`apply_meta_update`](super::apply_meta_update).
#[must_use]
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
Expand Down
113 changes: 113 additions & 0 deletions agent-client-protocol-schema/src/v2/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use std::sync::Arc;

use crate::MaybeUndefined;

/// Value attached to a given ACP type on the `_meta` field.
///
/// The _meta property is reserved by ACP to allow clients and agents to attach
Expand All @@ -13,6 +15,44 @@ use std::sync::Arc;
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
pub type Meta = serde_json::Map<String, serde_json::Value>;

/// Applies the top-level `_meta` patch of an upsert update to stored metadata.
///
/// An omitted `update` leaves `stored` unchanged, and `null` clears it. An
/// object patches `stored` one top-level key at a time: keys it omits keep
/// their stored values, a key set to `null` is removed, and any other value
/// replaces that key's stored value whole. Nested objects are not merged, and
/// a `null` nested inside a value is kept as data. Removing every key leaves an
/// empty object, which means the same as no metadata.
///
/// Use this for the top-level `_meta` of upsert updates such as
/// [`ToolCallUpdate`](super::ToolCallUpdate),
/// [`TerminalUpdate`](super::TerminalUpdate), message updates,
/// [`SessionInfoUpdate`](super::SessionInfoUpdate), and
/// [`CompactionUpdate`](super::CompactionUpdate). Other `_meta` fields are not
/// patches.
///
/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
pub fn apply_meta_update(stored: &mut MaybeUndefined<Meta>, update: MaybeUndefined<Meta>) {
match update {
MaybeUndefined::Undefined => {}
MaybeUndefined::Null => *stored = MaybeUndefined::Null,
MaybeUndefined::Value(patch) => {
let mut meta = match std::mem::take(stored) {
MaybeUndefined::Value(meta) => meta,
MaybeUndefined::Undefined | MaybeUndefined::Null => Meta::new(),
};
for (key, value) in patch {
if value.is_null() {
meta.remove(&key);
} else {
meta.insert(key, value);
}
}
*stored = MaybeUndefined::Value(meta);
}
}
}

/// Allows for sending an arbitrary request that is not part of the ACP spec.
/// Extension methods provide a way to add custom functionality while maintaining
/// protocol compatibility.
Expand Down Expand Up @@ -95,3 +135,76 @@ impl ExtNotification {
}
}
}

#[cfg(test)]
mod tests {
use serde_json::{from_value, json};

use super::*;

fn meta(value: serde_json::Value) -> MaybeUndefined<Meta> {
from_value(value).unwrap()
}

#[test]
fn meta_update_patches_top_level_keys() {
let mut stored = meta(json!({
"zed.dev/kept": 1,
"zed.dev/removed": true,
"acme.dev/nested": {"a": 1, "b": 2},
"acme.dev/list": [1, 2]
}));

apply_meta_update(
&mut stored,
meta(json!({
"zed.dev/removed": null,
"zed.dev/absent": null,
"acme.dev/nested": {"b": 3, "c": null},
"acme.dev/list": [3],
"acme.dev/added": []
})),
);

// Values replace the stored key whole instead of merging or appending,
// and nested nulls are data rather than removals.
assert_eq!(
stored,
meta(json!({
"zed.dev/kept": 1,
"acme.dev/nested": {"b": 3, "c": null},
"acme.dev/list": [3],
"acme.dev/added": []
}))
);
}

#[test]
fn meta_update_omitted_empty_and_null() {
let original = meta(json!({"zed.dev/kept": 1}));
let mut stored = original.clone();

apply_meta_update(&mut stored, MaybeUndefined::Undefined);
assert_eq!(stored, original);

// An empty patch changes no keys.
apply_meta_update(&mut stored, meta(json!({})));
assert_eq!(stored, original);

apply_meta_update(&mut stored, MaybeUndefined::Null);
assert_eq!(stored, MaybeUndefined::Null);
}

#[test]
fn meta_update_starts_from_empty_after_clear_or_on_first_update() {
for initial in [MaybeUndefined::Undefined, MaybeUndefined::Null] {
let mut stored = initial;
apply_meta_update(
&mut stored,
meta(json!({"zed.dev/added": 1, "zed.dev/removed": null})),
);
// A `null` key value is never stored.
assert_eq!(stored, meta(json!({"zed.dev/added": 1})));
}
}
}
Loading
Loading