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
102 changes: 68 additions & 34 deletions agent-client-protocol-schema/src/v2/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
}

impl UpdateSessionNotification {
/// Builds [`SessionNotification`] with the required notification fields set; optional fields start unset or empty.

Check warning on line 69 in agent-client-protocol-schema/src/v2/client.rs

View workflow job for this annotation

GitHub Actions / Build

unresolved link to `SessionNotification`
#[must_use]
pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
Self {
Expand Down Expand Up @@ -321,6 +321,9 @@
///
/// Agents send this notification to update session information like title or custom metadata.
/// 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.
#[serde_as]
#[skip_serializing_none]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
Expand All @@ -338,15 +341,18 @@
#[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. Implementations MUST NOT make assumptions about values at
/// these keys.
/// 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.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
#[serde_as(deserialize_as = "DefaultOnError")]
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
#[schemars(extend("x-deserialize-default-on-error" = true))]
#[serde(default)]
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
#[serde(
rename = "_meta",
default,
skip_serializing_if = "MaybeUndefined::is_undefined"
)]
pub meta: MaybeUndefined<Meta>,
}

impl SessionInfoUpdate {
Expand All @@ -371,13 +377,13 @@
}

/// 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.
/// 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.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
self.meta = meta.into_maybe_undefined();
self
}
}
Expand Down Expand Up @@ -736,7 +742,7 @@
}
}

/// A streamed item of content
/// A streamed item of message content.
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
Expand All @@ -752,7 +758,7 @@
pub content: 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.
/// these keys. This field is chunk-scoped.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
#[serde_as(deserialize_as = "DefaultOnError")]
Expand All @@ -775,7 +781,7 @@

/// 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.
/// these keys. This field is chunk-scoped.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
#[must_use]
Expand All @@ -787,11 +793,11 @@

/// A user message upsert.
///
/// Only [`UserMessage::message_id`] is required. Other fields have patch
/// semantics: omitted fields leave the existing message value unchanged, `null`
/// clears or unsets the value, and concrete values replace the previous value.
/// For a new `messageId`, omitted fields use client defaults. `content` is
/// replaced as a whole array; send `[]` or `null` to clear it.
/// Only [`UserMessage::message_id`] is required. `content` has patch semantics:
/// an omitted field leaves existing message content unchanged, `null` clears the
/// value, and a concrete array replaces the previous value. For a new
/// `messageId`, omitted fields use client defaults. `content` is replaced as a
/// whole array; send `[]` or `null` to clear it.
///
/// Message updates and chunks are applied in the order they are received. When
/// a `user_message` update includes `content`, that array replaces any content
Expand All @@ -813,7 +819,7 @@
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.
/// these keys. Omitted means no metadata update; `null` is an explicit clear signal.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
Expand Down Expand Up @@ -858,11 +864,11 @@

/// An agent message upsert.
///
/// Only [`AgentMessage::message_id`] is required. Other fields have patch
/// semantics: omitted fields leave the existing message value unchanged, `null`
/// clears or unsets the value, and concrete values replace the previous value.
/// For a new `messageId`, omitted fields use client defaults. `content` is
/// replaced as a whole array; send `[]` or `null` to clear it.
/// Only [`AgentMessage::message_id`] is required. `content` has patch semantics:
/// an omitted field leaves existing message content unchanged, `null` clears the
/// value, and a concrete array replaces the previous value. For a new
/// `messageId`, omitted fields use client defaults. `content` is replaced as a
/// whole array; send `[]` or `null` to clear it.
///
/// Message updates and chunks are applied in the order they are received. When
/// an `agent_message` update includes `content`, that array replaces any
Expand All @@ -884,7 +890,7 @@
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.
/// these keys. Omitted means no metadata update; `null` is an explicit clear signal.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
Expand Down Expand Up @@ -929,11 +935,11 @@

/// An agent thought or reasoning message upsert.
///
/// Only [`AgentThought::message_id`] is required. Other fields have patch
/// semantics: omitted fields leave the existing thought value unchanged, `null`
/// clears or unsets the value, and concrete values replace the previous value.
/// For a new `messageId`, omitted fields use client defaults. `content` is
/// replaced as a whole array; send `[]` or `null` to clear it.
/// Only [`AgentThought::message_id`] is required. `content` has patch semantics:
/// an omitted field leaves existing thought content unchanged, `null` clears the
/// value, and a concrete array replaces the previous value. For a new
/// `messageId`, omitted fields use client defaults. `content` is replaced as a
/// whole array; send `[]` or `null` to clear it.
///
/// Message updates and chunks are applied in the order they are received. When
/// an `agent_thought` update includes `content`, that array replaces any
Expand All @@ -955,7 +961,7 @@
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.
/// these keys. Omitted means no metadata update; `null` is an explicit clear signal.
///
/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
Expand Down Expand Up @@ -2104,7 +2110,7 @@
SessionInfoUpdate {
title: MaybeUndefined::Undefined,
updated_at: MaybeUndefined::Undefined,
meta: None
meta: MaybeUndefined::Undefined
}
);
assert_eq!(
Expand All @@ -2113,7 +2119,7 @@
SessionInfoUpdate {
title: MaybeUndefined::Null,
updated_at: MaybeUndefined::Null,
meta: None
meta: MaybeUndefined::Undefined
}
);
assert_eq!(
Expand All @@ -2124,14 +2130,42 @@
SessionInfoUpdate {
title: MaybeUndefined::Value("title".to_string()),
updated_at: MaybeUndefined::Value("timestamp".to_string()),
meta: None
meta: MaybeUndefined::Undefined
}
);

let clear_meta =
serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
assert_eq!(clear_meta.meta, MaybeUndefined::Null);

let mut meta = Meta::new();
meta.insert("source".to_string(), json!("session-info"));

assert_eq!(
serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
"source": "session-info"
}}))
.unwrap()
.meta,
MaybeUndefined::Value(meta.clone())
);

assert_eq!(
serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
json!({})
);

assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
json!({"_meta": null})
);

assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
json!({"_meta": {
"source": "session-info"
}})
);
assert_eq!(
serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
json!({"title": "title"})
Expand Down
35 changes: 30 additions & 5 deletions agent-client-protocol-schema/src/v2/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1235,7 +1235,7 @@ impl IntoV1 for super::SessionInfoUpdate {
Ok(crate::v1::SessionInfoUpdate {
title: title.into_v1()?,
updated_at: updated_at.into_v1()?,
meta: meta.into_v1()?,
meta: maybe_undefined_meta_into_v1_option("SessionInfoUpdate", meta)?,
})
}
}
Expand All @@ -1252,7 +1252,7 @@ impl IntoV2 for crate::v1::SessionInfoUpdate {
Ok(super::SessionInfoUpdate {
title: title.into_v2()?,
updated_at: updated_at.into_v2()?,
meta: meta.into_v2()?,
meta: option_into_v2_maybe_undefined(meta)?,
})
}
}
Expand Down Expand Up @@ -2380,6 +2380,19 @@ where
}
}

fn maybe_undefined_meta_into_v1_option(
context: &str,
value: crate::MaybeUndefined<super::Meta>,
) -> Result<Option<crate::v1::Meta>> {
match value {
crate::MaybeUndefined::Value(value) => Ok(Some(value.into_v1()?)),
crate::MaybeUndefined::Null => Err(ProtocolConversionError::new(format!(
"v2 {context} with null _meta cannot be represented in v1"
))),
crate::MaybeUndefined::Undefined => Ok(None),
}
}

fn option_into_v2_maybe_undefined<T>(value: Option<T>) -> Result<crate::MaybeUndefined<T::Output>>
where
T: IntoV2,
Expand Down Expand Up @@ -2441,7 +2454,7 @@ impl IntoV1 for super::ToolCallUpdate {
raw_input: maybe_undefined_value_into_v1_option(raw_input),
raw_output: maybe_undefined_value_into_v1_option(raw_output),
},
meta: meta.into_v1()?,
meta: maybe_undefined_meta_into_v1_option("ToolCallUpdate", meta)?,
})
}
}
Expand Down Expand Up @@ -2478,7 +2491,7 @@ impl IntoV2 for crate::v1::ToolCall {
locations: vec_into_v2_maybe_undefined_skip_errors(locations),
raw_input: option_into_v2_maybe_undefined(raw_input)?,
raw_output: option_into_v2_maybe_undefined(raw_output)?,
meta: meta.into_v2()?,
meta: option_into_v2_maybe_undefined(meta)?,
})
}
}
Expand Down Expand Up @@ -2510,7 +2523,7 @@ impl IntoV2 for crate::v1::ToolCallUpdate {
locations: option_vec_into_v2_maybe_undefined_skip_errors(locations),
raw_input: option_into_v2_maybe_undefined(raw_input)?,
raw_output: option_into_v2_maybe_undefined(raw_output)?,
meta: meta.into_v2()?,
meta: option_into_v2_maybe_undefined(meta)?,
})
}
}
Expand Down Expand Up @@ -9917,6 +9930,18 @@ mod tests {
assert_json_eq_after_v1_to_v2::<v1::ToolCallUpdate, v2::ToolCallUpdate>(update);
}

#[test]
fn v2_entity_meta_null_does_not_convert_to_v1() {
assert_v2_to_v1_error(
v2::SessionInfoUpdate::new().meta(None::<v2::Meta>),
"v2 SessionInfoUpdate with null _meta cannot be represented in v1",
);
assert_v2_to_v1_error(
v2::ToolCallUpdate::new("tc").meta(None::<v2::Meta>),
"v2 ToolCallUpdate with null _meta cannot be represented in v1",
);
}

#[test]
fn round_trips_session_notification_for_unchanged_update_kinds() {
fn content_chunk(text: &str, message_id: &str) -> v1::ContentChunk {
Expand Down
Loading