Skip to content

Commit b51ded3

Browse files
authored
fix(unstable): null clears session_message participant IDs and message references (#2555)
1 parent 027ace9 commit b51ded3

9 files changed

Lines changed: 178 additions & 125 deletions

File tree

‎agent-client-protocol-schema/src/v1/client.rs‎

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -244,34 +244,38 @@ impl SessionMessageChunk {
244244
pub struct SessionMessage {
245245
/// Identifier of this message within the enclosing session's transcript.
246246
pub message_id: MessageId,
247-
/// Optional sending session identity; omission or `null` retains a known value.
248-
#[serde_as(deserialize_as = "DefaultOnError")]
247+
/// The sending session.
248+
///
249+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it.
250+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
249251
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
250-
#[serde(default)]
251-
pub sender_session_id: Option<SessionId>,
252-
/// Optional receiving session identity; omission or `null` retains a known value.
253-
#[serde_as(deserialize_as = "DefaultOnError")]
252+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
253+
pub sender_session_id: MaybeUndefined<SessionId>,
254+
/// The receiving session.
255+
///
256+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it.
257+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
254258
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
255-
#[serde(default)]
256-
pub recipient_session_id: Option<SessionId>,
259+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
260+
pub recipient_session_id: MaybeUndefined<SessionId>,
257261
/// On an incoming view, the `messageId` of the same message's outgoing view in the
258262
/// sender's transcript.
259263
///
260-
/// Optional and nullable: omission or `null` means not supplied and retains a known
261-
/// value, so it can arrive after the message starts streaming.
262-
#[serde_as(deserialize_as = "DefaultOnError")]
264+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it. It can arrive
265+
/// after the message starts streaming.
266+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
263267
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
264-
#[serde(default)]
265-
pub sender_message_id: Option<MessageId>,
268+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
269+
pub sender_message_id: MaybeUndefined<MessageId>,
266270
/// On an outgoing view, the `messageId` of the same message's incoming view in the
267271
/// recipient's transcript.
268272
///
269-
/// Optional and nullable: omission or `null` means not supplied and retains a known
270-
/// value, so it can arrive after the message starts streaming.
271-
#[serde_as(deserialize_as = "DefaultOnError")]
273+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it. It can arrive
274+
/// after the message starts streaming.
275+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
272276
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
273-
#[serde(default)]
274-
pub recipient_message_id: Option<MessageId>,
277+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
278+
pub recipient_message_id: MaybeUndefined<MessageId>,
275279
/// Omitted leaves content unchanged; `null` or `[]` clears it.
276280
/// A non-empty array replaces all content.
277281
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
@@ -298,46 +302,52 @@ impl SessionMessage {
298302
pub fn new(message_id: impl Into<MessageId>) -> Self {
299303
Self {
300304
message_id: message_id.into(),
301-
sender_session_id: None,
302-
recipient_session_id: None,
303-
sender_message_id: None,
304-
recipient_message_id: None,
305+
sender_session_id: MaybeUndefined::Undefined,
306+
recipient_session_id: MaybeUndefined::Undefined,
307+
sender_message_id: MaybeUndefined::Undefined,
308+
recipient_message_id: MaybeUndefined::Undefined,
305309
content: MaybeUndefined::Undefined,
306310
meta: MaybeUndefined::Undefined,
307311
}
308312
}
309313

310-
/// Supplies the sending session identity, when known.
314+
/// Sets, clears, or leaves unchanged the sending session.
311315
#[must_use]
312-
pub fn sender_session_id(mut self, sender_session_id: impl IntoOption<SessionId>) -> Self {
313-
self.sender_session_id = sender_session_id.into_option();
316+
pub fn sender_session_id(
317+
mut self,
318+
sender_session_id: impl IntoMaybeUndefined<SessionId>,
319+
) -> Self {
320+
self.sender_session_id = sender_session_id.into_maybe_undefined();
314321
self
315322
}
316323

317-
/// Supplies the receiving session identity, when known.
324+
/// Sets, clears, or leaves unchanged the receiving session.
318325
#[must_use]
319326
pub fn recipient_session_id(
320327
mut self,
321-
recipient_session_id: impl IntoOption<SessionId>,
328+
recipient_session_id: impl IntoMaybeUndefined<SessionId>,
322329
) -> Self {
323-
self.recipient_session_id = recipient_session_id.into_option();
330+
self.recipient_session_id = recipient_session_id.into_maybe_undefined();
324331
self
325332
}
326333

327334
/// Links an incoming view to the outgoing view's message ID in the sender's transcript.
328335
#[must_use]
329-
pub fn sender_message_id(mut self, sender_message_id: impl IntoOption<MessageId>) -> Self {
330-
self.sender_message_id = sender_message_id.into_option();
336+
pub fn sender_message_id(
337+
mut self,
338+
sender_message_id: impl IntoMaybeUndefined<MessageId>,
339+
) -> Self {
340+
self.sender_message_id = sender_message_id.into_maybe_undefined();
331341
self
332342
}
333343

334344
/// Links an outgoing view to the incoming view's message ID in the recipient's transcript.
335345
#[must_use]
336346
pub fn recipient_message_id(
337347
mut self,
338-
recipient_message_id: impl IntoOption<MessageId>,
348+
recipient_message_id: impl IntoMaybeUndefined<MessageId>,
339349
) -> Self {
340-
self.recipient_message_id = recipient_message_id.into_option();
350+
self.recipient_message_id = recipient_message_id.into_maybe_undefined();
341351
self
342352
}
343353

@@ -574,11 +584,20 @@ mod session_message_tests {
574584
let enriched = SessionMessage::new("m1")
575585
.sender_session_id(SessionId::new("parent"))
576586
.recipient_session_id(SessionId::new("child"));
577-
assert_eq!(enriched.sender_session_id, Some(SessionId::new("parent")));
578-
assert_eq!(enriched.recipient_session_id, Some(SessionId::new("child")));
587+
assert_eq!(
588+
enriched.sender_session_id,
589+
MaybeUndefined::Value(SessionId::new("parent"))
590+
);
591+
assert_eq!(
592+
enriched.recipient_session_id,
593+
MaybeUndefined::Value(SessionId::new("child"))
594+
);
579595
let later = SessionMessage::new("m1").sender_session_id(SessionId::new("parent"));
580-
assert_eq!(later.sender_session_id, Some(SessionId::new("parent")));
581-
assert_eq!(later.recipient_session_id, None);
596+
assert_eq!(
597+
later.sender_session_id,
598+
MaybeUndefined::Value(SessionId::new("parent"))
599+
);
600+
assert_eq!(later.recipient_session_id, MaybeUndefined::Undefined);
582601
let more = SessionMessageChunk::new("m1", block);
583602
for update in [
584603
SessionUpdate::SessionMessage(minimal),
@@ -594,12 +613,11 @@ mod session_message_tests {
594613
}
595614

596615
#[test]
597-
fn invalid_or_null_endpoints_are_absent_but_message_id_is_required() {
616+
fn null_endpoints_clear_invalid_are_absent_but_message_id_is_required() {
598617
let mut base = json!({"sessionUpdate": "session_message", "messageId": "m1",
599618
"senderSessionId": null, "recipientSessionId": 42});
600619
let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap();
601620
let encoded = serde_json::to_value(decoded).unwrap();
602-
base.as_object_mut().unwrap().remove("senderSessionId");
603621
base.as_object_mut().unwrap().remove("recipientSessionId");
604622
assert_eq!(encoded, base);
605623
for bad in [Value::Null, json!(42)] {
@@ -641,12 +659,11 @@ mod session_message_tests {
641659
let mut wire = json!({"sessionUpdate": "session_message", "messageId": "m1",
642660
"senderMessageId": null, "recipientMessageId": 42});
643661
let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
644-
wire.as_object_mut().unwrap().remove("senderMessageId");
645662
wire.as_object_mut().unwrap().remove("recipientMessageId");
646663
assert_eq!(
647664
serde_json::to_value(decoded).unwrap(),
648665
wire,
649-
"null or malformed references mean not supplied"
666+
"null clears a reference and a malformed one is not supplied"
650667
);
651668
}
652669
}

‎agent-client-protocol-schema/src/v2/client.rs‎

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -254,34 +254,38 @@ impl SessionMessageChunk {
254254
pub struct SessionMessage {
255255
/// Identifier of this message within the enclosing session's transcript.
256256
pub message_id: MessageId,
257-
/// Optional sending session identity; omission or `null` retains a known value.
258-
#[serde_as(deserialize_as = "DefaultOnError")]
257+
/// The sending session.
258+
///
259+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it.
260+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
259261
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
260-
#[serde(default)]
261-
pub sender_session_id: Option<SessionId>,
262-
/// Optional receiving session identity; omission or `null` retains a known value.
263-
#[serde_as(deserialize_as = "DefaultOnError")]
262+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
263+
pub sender_session_id: MaybeUndefined<SessionId>,
264+
/// The receiving session.
265+
///
266+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it.
267+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
264268
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
265-
#[serde(default)]
266-
pub recipient_session_id: Option<SessionId>,
269+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
270+
pub recipient_session_id: MaybeUndefined<SessionId>,
267271
/// On an incoming view, the `messageId` of the same message's outgoing view in the
268272
/// sender's transcript.
269273
///
270-
/// Optional and nullable: omission or `null` means not supplied and retains a known
271-
/// value, so it can arrive after the message starts streaming.
272-
#[serde_as(deserialize_as = "DefaultOnError")]
274+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it. It can arrive
275+
/// after the message starts streaming.
276+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
273277
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
274-
#[serde(default)]
275-
pub sender_message_id: Option<MessageId>,
278+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
279+
pub sender_message_id: MaybeUndefined<MessageId>,
276280
/// On an outgoing view, the `messageId` of the same message's incoming view in the
277281
/// recipient's transcript.
278282
///
279-
/// Optional and nullable: omission or `null` means not supplied and retains a known
280-
/// value, so it can arrive after the message starts streaming.
281-
#[serde_as(deserialize_as = "DefaultOnError")]
283+
/// Omitted leaves the stored value unchanged; `null` clears it; a value replaces it. It can arrive
284+
/// after the message starts streaming.
285+
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
282286
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
283-
#[serde(default)]
284-
pub recipient_message_id: Option<MessageId>,
287+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
288+
pub recipient_message_id: MaybeUndefined<MessageId>,
285289
/// Omitted leaves content unchanged; `null` clears it; a concrete array
286290
/// replaces the whole content collection.
287291
#[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
@@ -313,46 +317,52 @@ impl SessionMessage {
313317
pub fn new(message_id: impl Into<MessageId>) -> Self {
314318
Self {
315319
message_id: message_id.into(),
316-
sender_session_id: None,
317-
recipient_session_id: None,
318-
sender_message_id: None,
319-
recipient_message_id: None,
320+
sender_session_id: MaybeUndefined::Undefined,
321+
recipient_session_id: MaybeUndefined::Undefined,
322+
sender_message_id: MaybeUndefined::Undefined,
323+
recipient_message_id: MaybeUndefined::Undefined,
320324
content: MaybeUndefined::Undefined,
321325
meta: MaybeUndefined::Undefined,
322326
}
323327
}
324328

325-
/// Supplies the sending session identity, when known.
329+
/// Sets, clears, or leaves unchanged the sending session.
326330
#[must_use]
327-
pub fn sender_session_id(mut self, sender_session_id: impl IntoOption<SessionId>) -> Self {
328-
self.sender_session_id = sender_session_id.into_option();
331+
pub fn sender_session_id(
332+
mut self,
333+
sender_session_id: impl IntoMaybeUndefined<SessionId>,
334+
) -> Self {
335+
self.sender_session_id = sender_session_id.into_maybe_undefined();
329336
self
330337
}
331338

332-
/// Supplies the receiving session identity, when known.
339+
/// Sets, clears, or leaves unchanged the receiving session.
333340
#[must_use]
334341
pub fn recipient_session_id(
335342
mut self,
336-
recipient_session_id: impl IntoOption<SessionId>,
343+
recipient_session_id: impl IntoMaybeUndefined<SessionId>,
337344
) -> Self {
338-
self.recipient_session_id = recipient_session_id.into_option();
345+
self.recipient_session_id = recipient_session_id.into_maybe_undefined();
339346
self
340347
}
341348

342349
/// Links an incoming view to the outgoing view's message ID in the sender's transcript.
343350
#[must_use]
344-
pub fn sender_message_id(mut self, sender_message_id: impl IntoOption<MessageId>) -> Self {
345-
self.sender_message_id = sender_message_id.into_option();
351+
pub fn sender_message_id(
352+
mut self,
353+
sender_message_id: impl IntoMaybeUndefined<MessageId>,
354+
) -> Self {
355+
self.sender_message_id = sender_message_id.into_maybe_undefined();
346356
self
347357
}
348358

349359
/// Links an outgoing view to the incoming view's message ID in the recipient's transcript.
350360
#[must_use]
351361
pub fn recipient_message_id(
352362
mut self,
353-
recipient_message_id: impl IntoOption<MessageId>,
363+
recipient_message_id: impl IntoMaybeUndefined<MessageId>,
354364
) -> Self {
355-
self.recipient_message_id = recipient_message_id.into_option();
365+
self.recipient_message_id = recipient_message_id.into_maybe_undefined();
356366
self
357367
}
358368

@@ -556,11 +566,20 @@ mod session_message_tests {
556566
let enriched = SessionMessage::new("m1")
557567
.sender_session_id(SessionId::new("parent"))
558568
.recipient_session_id(SessionId::new("child"));
559-
assert_eq!(enriched.sender_session_id, Some(SessionId::new("parent")));
560-
assert_eq!(enriched.recipient_session_id, Some(SessionId::new("child")));
569+
assert_eq!(
570+
enriched.sender_session_id,
571+
MaybeUndefined::Value(SessionId::new("parent"))
572+
);
573+
assert_eq!(
574+
enriched.recipient_session_id,
575+
MaybeUndefined::Value(SessionId::new("child"))
576+
);
561577
let later = SessionMessage::new("m1").sender_session_id(SessionId::new("parent"));
562-
assert_eq!(later.sender_session_id, Some(SessionId::new("parent")));
563-
assert_eq!(later.recipient_session_id, None);
578+
assert_eq!(
579+
later.sender_session_id,
580+
MaybeUndefined::Value(SessionId::new("parent"))
581+
);
582+
assert_eq!(later.recipient_session_id, MaybeUndefined::Undefined);
564583
let more = SessionMessageChunk::new("m1", block);
565584
for update in [
566585
SessionUpdate::SessionMessage(minimal),
@@ -576,12 +595,11 @@ mod session_message_tests {
576595
}
577596

578597
#[test]
579-
fn invalid_or_null_endpoints_are_absent_but_message_id_is_required() {
598+
fn null_endpoints_clear_invalid_are_absent_but_message_id_is_required() {
580599
let mut base = json!({"sessionUpdate": "session_message", "messageId": "m1",
581600
"senderSessionId": null, "recipientSessionId": 42});
582601
let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap();
583602
let encoded = serde_json::to_value(decoded).unwrap();
584-
base.as_object_mut().unwrap().remove("senderSessionId");
585603
base.as_object_mut().unwrap().remove("recipientSessionId");
586604
assert_eq!(encoded, base);
587605
for bad in [Value::Null, json!(42)] {
@@ -623,12 +641,11 @@ mod session_message_tests {
623641
let mut wire = json!({"sessionUpdate": "session_message", "messageId": "m1",
624642
"senderMessageId": null, "recipientMessageId": 42});
625643
let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
626-
wire.as_object_mut().unwrap().remove("senderMessageId");
627644
wire.as_object_mut().unwrap().remove("recipientMessageId");
628645
assert_eq!(
629646
serde_json::to_value(decoded).unwrap(),
630647
wire,
631-
"null or malformed references mean not supplied"
648+
"null clears a reference and a malformed one is not supplied"
632649
);
633650
}
634651
}

‎docs/protocol/v1/draft/prompt-turn.mdx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ message-level patch.
9292

9393
Clients can render "To …" with a recipient link or "From …" with a sender link.
9494
Without sufficient participant metadata, use a generic inter-session
95-
presentation and add links when the metadata arrives. Omitted or `null`
96-
participant IDs mean not supplied and leave previously known identities intact.
95+
presentation and add links when the metadata arrives. Participant IDs follow the same patch rule as `content`: omission leaves a known value unchanged and `null` clears it.
9796
The two views have independent message IDs. Optional `senderMessageId` on the
9897
incoming view and `recipientMessageId` on the outgoing view name the other
9998
view's `messageId`, so a Client can go from either view to the other. They

0 commit comments

Comments
 (0)