From 0a7e5aaad680bab6c9c4a932e70c51b3d5f7a843 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 23 Oct 2026 11:34:05 +0200 Subject: [PATCH 1/3] docs(rfd): link both views of a session-directed message Add optional senderMessageId on the incoming view and recipientMessageId on the outgoing view, so a Client can go from either view to the other without inferring a link from equal message IDs. Either reference can be supplied after the view starts streaming. --- docs/rfds/subagents.mdx | 63 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/rfds/subagents.mdx b/docs/rfds/subagents.mdx index fc6a6b826..2c1566287 100644 --- a/docs/rfds/subagents.mdx +++ b/docs/rfds/subagents.mdx @@ -266,6 +266,11 @@ necessarily the sender. Each update has: when supplied. - `recipientSessionId`: optional and nullable. Identifies the receiving session when supplied. +- `senderMessageId`: optional and nullable. On an incoming view, the + `messageId` of the same message's outgoing view in the sender's transcript. +- `recipientMessageId`: optional and nullable. On an outgoing view, the + `messageId` of the same message's incoming view in the recipient's + transcript. - `content`: a `session_message` may replace the whole `ContentBlock[]`; `session_message_chunk` requires one non-nullable `ContentBlock` to append. Normal text, images, audio, and resource content use their existing shapes. @@ -278,6 +283,20 @@ identities. For these identity fields, omission and `null` mean not supplied: the Client retains any previously reported value. They do not use the null-clearing semantics of the mutable `content` and `_meta` fields. +`senderMessageId` and `recipientMessageId` link the two views of a message so +that a Client looking at either one can find the other, including when it has +not yet received or rendered the other transcript's entry. They follow the +same convention as the participant IDs: omission and `null` mean not supplied, +the Client retains any previously reported value, and a known value does not +change. An Agent can therefore stream a view before it knows the other one's +ID and supply the reference later with a chunk or a metadata-only upsert. For +example, it can add `recipientMessageId` to the outgoing view once it has +reported the incoming view. Agents **SHOULD** supply each reference once they +know it. When both views carry references, they **MUST** name each other; a +Client that receives conflicting references treats the views as unlinked. A +reference does not indicate that the recipient finished processing the +message, and it does not establish ownership. + Known participant information must remain consistent with the enclosing transcript and earlier updates for that message. When both IDs are known, they identify different sessions and the outer `params.sessionId` matches one @@ -342,6 +361,7 @@ same; the outer session and transcript-local message ID change: "messageId": "msg_from_parent_1", "senderSessionId": "sess_parent", "recipientSessionId": "sess_child_1", + "senderMessageId": "msg_to_child_1", "content": [ { "type": "text", @@ -353,6 +373,24 @@ same; the outer session and transcript-local message ID change: } ``` +The Agent can then link the parent's outgoing view to it with a metadata-only +upsert: + +```json +{ + "jsonrpc": "2.0", + "method": "session/update", + "params": { + "sessionId": "sess_parent", + "update": { + "sessionUpdate": "session_message", + "messageId": "msg_to_child_1", + "recipientMessageId": "msg_from_parent_1" + } + } +} +``` + These notifications report Agent-owned communication; the Client does not forward the content or invoke `session/prompt` because it received one. Neither form acknowledges recipient processing or establishes new ownership. Actual @@ -365,7 +403,7 @@ entry solely from an outgoing entry. This does not prevent a clearly labeled projection of outgoing messages in a cross-session view. The two views have independently scoped message IDs, and Clients **MUST NOT** assume equal IDs identify the same communication across sessions. -No cross-session message-correlation ID is defined here. +Only `senderMessageId` and `recipientMessageId` link the two views. An incoming entry uses this message kind, not an additional ordinary user message that would misattribute the communication. Rendering the same entry @@ -450,7 +488,8 @@ For example, with parent `sess_parent` and child `sess_child_1`: This updates the parent's history with a link to the child. 3. The received view has outer `sessionId: "sess_child_1"` and the same sender and recipient. Its independent message ID identifies the entry in the - child's history, where the Client can link back to the parent. + child's history, and its `senderMessageId` links back to the parent's + outgoing view, which can name it with `recipientMessageId`. 4. The child's own messages, plans, and tool calls have outer `sessionId: "sess_child_1"`. They update the child's history, not the parent session. V2 work-state notifications use this child-addressed stream @@ -1017,6 +1056,11 @@ implementation of child routing or the replay freshness boundary. invents an Agent-reported incoming entry from outgoing traffic alone. Clearly labeled UI projections remain possible, without duplicating the communication as an ordinary user message. +- Stream an incoming view before its outgoing view's ID is known, then supply + `senderMessageId` on the incoming view and `recipientMessageId` on the + outgoing view. Verify that a Client can go from either view to the other, + including before it has rendered the other transcript, and that it does not + link views by equal IDs alone. - Start a directed message with a chunk, interleave another message, and replace accumulated content with a whole snapshot. Start without participant metadata, add it later without replacing content, and omit it on subsequent @@ -1161,6 +1205,17 @@ provide that representation and support streaming. Actual tool operations can still be reported normally, but their status does not define a child's lifetime or acknowledge that it processed a message. +### Why can both views of a message reference each other? + +A Client showing one transcript may need the other view's entry without having +received or rendered it, for example to open a child's conversation at the +message a parent sent. One reference would force that Client to search the +other transcript first, so each view can name the other. Both are optional and +can arrive in later updates, because an Agent may start streaming a view +before it knows the other one's ID. Message IDs stay scoped to their +transcript; requiring both views to share an ID would instead force the Agent +to know the link before sending the first update of either view. + ### Why not allow prompting or steering a subagent? The parent Agent can message or reuse a child according to its runtime's @@ -1213,6 +1268,10 @@ important difference from a user-facing session. ## Revision history +- 2026-10-22: Added optional `senderMessageId` on the incoming view and + `recipientMessageId` on the outgoing view of a session-directed message, so + a Client can go from either view to the other without inferring a link from + equal IDs. Either can be supplied after the view starts streaming. - 2026-10-13: In v2, a concrete top-level `_meta` object on `subagent_update` and `session_message` patches stored metadata by key, per [v2 Metadata Patches](/rfds/v2/meta-patches). v1 still replaces the whole From dd0b711a34304b512383ced931e69571de3d87eb Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 23 Oct 2026 11:34:06 +0200 Subject: [PATCH 2/3] feat(unstable): add message references to session messages SessionMessage and SessionMessageChunk gain optional senderMessageId and recipientMessageId in v1 and v2, following the participant-ID rules: omitted or null means not supplied and retains a known value, and a malformed value is treated as not supplied. --- agent-client-protocol-schema/src/v1/client.rs | 124 ++++++++++++++++++ agent-client-protocol-schema/src/v2/client.rs | 124 ++++++++++++++++++ docs/protocol/v1/draft/prompt-turn.mdx | 6 +- docs/protocol/v1/draft/schema.mdx | 64 +++++++++ docs/protocol/v2/draft/prompt-lifecycle.mdx | 6 +- docs/protocol/v2/draft/schema.mdx | 64 +++++++++ schema/v1/schema.unstable.json | 48 +++++++ schema/v2/schema.unstable.json | 48 +++++++ 8 files changed, 482 insertions(+), 2 deletions(-) diff --git a/agent-client-protocol-schema/src/v1/client.rs b/agent-client-protocol-schema/src/v1/client.rs index c726f76e1..00cdeee9e 100644 --- a/agent-client-protocol-schema/src/v1/client.rs +++ b/agent-client-protocol-schema/src/v1/client.rs @@ -205,6 +205,24 @@ pub struct SessionMessageChunk { #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] pub recipient_session_id: Option, + /// On an incoming view, the `messageId` of the same message's outgoing view in the + /// sender's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub sender_message_id: Option, + /// On an outgoing view, the `messageId` of the same message's incoming view in the + /// recipient's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub recipient_message_id: Option, /// A single content block appended to the message. pub content: ContentBlock, /// Optional and nullable chunk-scoped metadata; omitted or `null` means none. @@ -225,6 +243,8 @@ impl SessionMessageChunk { message_id: message_id.into(), sender_session_id: None, recipient_session_id: None, + sender_message_id: None, + recipient_message_id: None, content, meta: None, } @@ -247,6 +267,23 @@ impl SessionMessageChunk { self } + /// Links an incoming view to the outgoing view's message ID in the sender's transcript. + #[must_use] + pub fn sender_message_id(mut self, sender_message_id: impl IntoOption) -> Self { + self.sender_message_id = sender_message_id.into_option(); + self + } + + /// Links an outgoing view to the incoming view's message ID in the recipient's transcript. + #[must_use] + pub fn recipient_message_id( + mut self, + recipient_message_id: impl IntoOption, + ) -> Self { + self.recipient_message_id = recipient_message_id.into_option(); + self + } + /// Sets optional chunk-scoped metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { @@ -280,6 +317,24 @@ pub struct SessionMessage { #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] pub recipient_session_id: Option, + /// On an incoming view, the `messageId` of the same message's outgoing view in the + /// sender's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub sender_message_id: Option, + /// On an outgoing view, the `messageId` of the same message's incoming view in the + /// recipient's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub recipient_message_id: Option, /// Omitted leaves content unchanged; `null` or `[]` clears it. /// A non-empty array replaces all content. #[serde_as(deserialize_as = "DefaultOnError>>")] @@ -308,6 +363,8 @@ impl SessionMessage { message_id: message_id.into(), sender_session_id: None, recipient_session_id: None, + sender_message_id: None, + recipient_message_id: None, content: MaybeUndefined::Undefined, meta: MaybeUndefined::Undefined, } @@ -330,6 +387,23 @@ impl SessionMessage { self } + /// Links an incoming view to the outgoing view's message ID in the sender's transcript. + #[must_use] + pub fn sender_message_id(mut self, sender_message_id: impl IntoOption) -> Self { + self.sender_message_id = sender_message_id.into_option(); + self + } + + /// Links an outgoing view to the incoming view's message ID in the recipient's transcript. + #[must_use] + pub fn recipient_message_id( + mut self, + recipient_message_id: impl IntoOption, + ) -> Self { + self.recipient_message_id = recipient_message_id.into_option(); + self + } + /// Replaces, clears, or omits the complete content patch. #[must_use] pub fn content(mut self, content: impl IntoMaybeUndefined>) -> Self { @@ -602,6 +676,56 @@ mod session_message_tests { } } } + + #[test] + fn message_references_link_views_and_can_arrive_late() { + let block = ContentBlock::Text(crate::v1::TextContent::new("hello")); + let incoming = SessionMessageChunk::new("received-1", block) + .sender_session_id(SessionId::new("parent")) + .recipient_session_id(SessionId::new("child")) + .sender_message_id(MessageId::new("sent-1")); + let outgoing = + SessionMessage::new("sent-1").recipient_message_id(MessageId::new("received-1")); + for (update, wire) in [ + ( + SessionUpdate::SessionMessageChunk(incoming), + json!({"sessionUpdate": "session_message_chunk", "messageId": "received-1", + "senderSessionId": "parent", "recipientSessionId": "child", + "senderMessageId": "sent-1", "content": {"type": "text", "text": "hello"}}), + ), + ( + SessionUpdate::SessionMessage(outgoing), + json!({"sessionUpdate": "session_message", "messageId": "sent-1", + "recipientMessageId": "received-1"}), + ), + ] { + assert_eq!(serde_json::to_value(&update).unwrap(), wire); + let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(decoded, update); + } + + for (kind, content) in [ + ("session_message", None), + ( + "session_message_chunk", + Some(json!({"type": "text", "text": "hello"})), + ), + ] { + let mut wire = json!({"sessionUpdate": kind, "messageId": "m1", + "senderMessageId": null, "recipientMessageId": 42}); + if let Some(content) = content { + wire["content"] = content; + } + let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); + wire.as_object_mut().unwrap().remove("senderMessageId"); + wire.as_object_mut().unwrap().remove("recipientMessageId"); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + wire, + "{kind}: null or malformed references mean not supplied" + ); + } + } } #[cfg(all(test, not(feature = "unstable_subagents")))] diff --git a/agent-client-protocol-schema/src/v2/client.rs b/agent-client-protocol-schema/src/v2/client.rs index b5cf1a0bf..8d28a0995 100644 --- a/agent-client-protocol-schema/src/v2/client.rs +++ b/agent-client-protocol-schema/src/v2/client.rs @@ -215,6 +215,24 @@ pub struct SessionMessageChunk { #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] pub recipient_session_id: Option, + /// On an incoming view, the `messageId` of the same message's outgoing view in the + /// sender's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub sender_message_id: Option, + /// On an outgoing view, the `messageId` of the same message's incoming view in the + /// recipient's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub recipient_message_id: Option, /// A single content block appended to the message. pub content: ContentBlock, /// Optional and nullable chunk-scoped metadata; omitted or `null` means none. @@ -235,6 +253,8 @@ impl SessionMessageChunk { message_id: message_id.into(), sender_session_id: None, recipient_session_id: None, + sender_message_id: None, + recipient_message_id: None, content, meta: None, } @@ -257,6 +277,23 @@ impl SessionMessageChunk { self } + /// Links an incoming view to the outgoing view's message ID in the sender's transcript. + #[must_use] + pub fn sender_message_id(mut self, sender_message_id: impl IntoOption) -> Self { + self.sender_message_id = sender_message_id.into_option(); + self + } + + /// Links an outgoing view to the incoming view's message ID in the recipient's transcript. + #[must_use] + pub fn recipient_message_id( + mut self, + recipient_message_id: impl IntoOption, + ) -> Self { + self.recipient_message_id = recipient_message_id.into_option(); + self + } + /// Sets optional chunk-scoped metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { @@ -290,6 +327,24 @@ pub struct SessionMessage { #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] pub recipient_session_id: Option, + /// On an incoming view, the `messageId` of the same message's outgoing view in the + /// sender's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub sender_message_id: Option, + /// On an outgoing view, the `messageId` of the same message's incoming view in the + /// recipient's transcript. + /// + /// Optional and nullable: omission or `null` means not supplied and retains a known + /// value, so it can arrive after the message starts streaming. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub recipient_message_id: Option, /// Omitted leaves content unchanged; `null` clears it; a concrete array /// replaces the whole content collection. #[serde_as(deserialize_as = "DefaultOnError>>")] @@ -323,6 +378,8 @@ impl SessionMessage { message_id: message_id.into(), sender_session_id: None, recipient_session_id: None, + sender_message_id: None, + recipient_message_id: None, content: MaybeUndefined::Undefined, meta: MaybeUndefined::Undefined, } @@ -345,6 +402,23 @@ impl SessionMessage { self } + /// Links an incoming view to the outgoing view's message ID in the sender's transcript. + #[must_use] + pub fn sender_message_id(mut self, sender_message_id: impl IntoOption) -> Self { + self.sender_message_id = sender_message_id.into_option(); + self + } + + /// Links an outgoing view to the incoming view's message ID in the recipient's transcript. + #[must_use] + pub fn recipient_message_id( + mut self, + recipient_message_id: impl IntoOption, + ) -> Self { + self.recipient_message_id = recipient_message_id.into_option(); + self + } + /// Replaces, clears, or leaves unchanged the message content. #[must_use] pub fn content(mut self, content: impl IntoMaybeUndefined>) -> Self { @@ -580,6 +654,56 @@ mod session_message_tests { } } } + + #[test] + fn message_references_link_views_and_can_arrive_late() { + let block = ContentBlock::Text(crate::v2::TextContent::new("hello")); + let incoming = SessionMessageChunk::new("received-1", block) + .sender_session_id(SessionId::new("parent")) + .recipient_session_id(SessionId::new("child")) + .sender_message_id(MessageId::new("sent-1")); + let outgoing = + SessionMessage::new("sent-1").recipient_message_id(MessageId::new("received-1")); + for (update, wire) in [ + ( + SessionUpdate::SessionMessageChunk(incoming), + json!({"sessionUpdate": "session_message_chunk", "messageId": "received-1", + "senderSessionId": "parent", "recipientSessionId": "child", + "senderMessageId": "sent-1", "content": {"type": "text", "text": "hello"}}), + ), + ( + SessionUpdate::SessionMessage(outgoing), + json!({"sessionUpdate": "session_message", "messageId": "sent-1", + "recipientMessageId": "received-1"}), + ), + ] { + assert_eq!(serde_json::to_value(&update).unwrap(), wire); + let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(decoded, update); + } + + for (kind, content) in [ + ("session_message", None), + ( + "session_message_chunk", + Some(json!({"type": "text", "text": "hello"})), + ), + ] { + let mut wire = json!({"sessionUpdate": kind, "messageId": "m1", + "senderMessageId": null, "recipientMessageId": 42}); + if let Some(content) = content { + wire["content"] = content; + } + let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); + wire.as_object_mut().unwrap().remove("senderMessageId"); + wire.as_object_mut().unwrap().remove("recipientMessageId"); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + wire, + "{kind}: null or malformed references mean not supplied" + ); + } + } } #[cfg(all(test, not(feature = "unstable_subagents")))] diff --git a/docs/protocol/v1/draft/prompt-turn.mdx b/docs/protocol/v1/draft/prompt-turn.mdx index 81dd60811..1c86e353a 100644 --- a/docs/protocol/v1/draft/prompt-turn.mdx +++ b/docs/protocol/v1/draft/prompt-turn.mdx @@ -77,7 +77,11 @@ Clients can render "To …" with a recipient link or "From …" with a sender li Without sufficient participant metadata, use a generic inter-session presentation and add links when the metadata arrives. Omitted or `null` participant IDs mean not supplied and leave previously known identities intact. -The two views have independent message IDs. The Agent reports each side only +The two views have independent message IDs. Optional `senderMessageId` on the +incoming view and `recipientMessageId` on the outgoing view name the other +view's `messageId`, so a Client can go from either view to the other. They +follow the participant-ID rules and can arrive in a later update; Clients must +not link views by equal IDs alone. The Agent reports each side only when observed; Clients must not synthesize an incoming entry from an outgoing one or forward the content themselves. Neither view acknowledges completed recipient processing. See the [Subagent Sessions RFD](/rfds/subagents#session-directed-messages) diff --git a/docs/protocol/v1/draft/schema.mdx b/docs/protocol/v1/draft/schema.mdx index 054ab3db6..5668fdf35 100644 --- a/docs/protocol/v1/draft/schema.mdx +++ b/docs/protocol/v1/draft/schema.mdx @@ -6459,9 +6459,25 @@ A non-empty array replaces all content. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. @@ -6490,9 +6506,25 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. @@ -7278,9 +7310,25 @@ A non-empty array replaces all content. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. @@ -7312,9 +7360,25 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. diff --git a/docs/protocol/v2/draft/prompt-lifecycle.mdx b/docs/protocol/v2/draft/prompt-lifecycle.mdx index 014a1b84c..3ebe91aa5 100644 --- a/docs/protocol/v2/draft/prompt-lifecycle.mdx +++ b/docs/protocol/v2/draft/prompt-lifecycle.mdx @@ -108,7 +108,11 @@ Clients can render "To …" with a recipient link or "From …" with a sender li Without sufficient participant metadata, use a generic inter-session presentation and add links when the metadata arrives. Omitted or `null` participant IDs mean not supplied and leave previously known identities intact. -The two views have independent message IDs. The Agent reports each side only +The two views have independent message IDs. Optional `senderMessageId` on the +incoming view and `recipientMessageId` on the outgoing view name the other +view's `messageId`, so a Client can go from either view to the other. They +follow the participant-ID rules and can arrive in a later update; Clients must +not link views by equal IDs alone. The Agent reports each side only when observed; Clients must not synthesize an incoming entry from an outgoing one or forward the content themselves. Neither view acknowledges completed recipient processing. diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index 4c00f2dda..709250bb5 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -6768,9 +6768,25 @@ replaces the whole content collection. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. @@ -6799,9 +6815,25 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. @@ -7858,9 +7890,25 @@ replaces the whole content collection. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. @@ -7892,9 +7940,25 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. + +MessageId | null} > + On an outgoing view, the `messageId` of the same message's incoming view in the +recipient's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional receiving session identity; omission or `null` retains a known value. + +MessageId | null} > + On an incoming view, the `messageId` of the same message's outgoing view in the +sender's transcript. + +Optional and nullable: omission or `null` means not supplied and retains a known +value, so it can arrive after the message starts streaming. + SessionId | null} > Optional sending session identity; omission or `null` retains a known value. diff --git a/schema/v1/schema.unstable.json b/schema/v1/schema.unstable.json index 11f244ce1..6c0771882 100644 --- a/schema/v1/schema.unstable.json +++ b/schema/v1/schema.unstable.json @@ -5695,6 +5695,30 @@ ], "x-deserialize-default-on-error": true }, + "senderMessageId": { + "description": "On an incoming view, the `messageId` of the same message's outgoing view in the\nsender's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "recipientMessageId": { + "description": "On an outgoing view, the `messageId` of the same message's incoming view in the\nrecipient's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, "content": { "description": "Omitted leaves content unchanged; `null` or `[]` clears it.\nA non-empty array replaces all content.", "type": ["array", "null"], @@ -5749,6 +5773,30 @@ ], "x-deserialize-default-on-error": true }, + "senderMessageId": { + "description": "On an incoming view, the `messageId` of the same message's outgoing view in the\nsender's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "recipientMessageId": { + "description": "On an outgoing view, the `messageId` of the same message's incoming view in the\nrecipient's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, "content": { "description": "A single content block appended to the message.", "allOf": [ diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 8a3264270..2cedde5bb 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -6957,6 +6957,30 @@ ], "x-deserialize-default-on-error": true }, + "senderMessageId": { + "description": "On an incoming view, the `messageId` of the same message's outgoing view in the\nsender's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "recipientMessageId": { + "description": "On an outgoing view, the `messageId` of the same message's incoming view in the\nrecipient's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, "content": { "description": "Omitted leaves content unchanged; `null` clears it; a concrete array\nreplaces the whole content collection.", "type": ["array", "null"], @@ -7011,6 +7035,30 @@ ], "x-deserialize-default-on-error": true }, + "senderMessageId": { + "description": "On an incoming view, the `messageId` of the same message's outgoing view in the\nsender's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "recipientMessageId": { + "description": "On an outgoing view, the `messageId` of the same message's incoming view in the\nrecipient's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", + "anyOf": [ + { + "$ref": "#/$defs/MessageId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, "content": { "description": "A single content block appended to the message.", "allOf": [ From 3bf3407ced7d6f37a25717891720d005160ff5be Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 23 Oct 2026 13:26:39 +0200 Subject: [PATCH 3/3] feat(unstable): keep message identity off session message chunks A session_message_chunk now carries only content, as agent_message_chunk does. The participant IDs and the new message references live only on session_message upserts; an Agent that learns them while streaming sends a metadata-only upsert. This gives each field one source and keeps chunks from patching message state, which their _meta already does not. --- agent-client-protocol-schema/src/v1/client.rs | 171 +++++------------ agent-client-protocol-schema/src/v2/client.rs | 177 +++++------------- docs/protocol/v1/draft/prompt-turn.mdx | 31 ++- docs/protocol/v1/draft/schema.mdx | 47 +---- docs/protocol/v2/draft/prompt-lifecycle.mdx | 31 ++- docs/protocol/v2/draft/schema.mdx | 47 +---- docs/rfds/subagents.mdx | 85 ++++++--- schema/v1/schema.unstable.json | 50 +---- schema/v2/schema.unstable.json | 50 +---- 9 files changed, 210 insertions(+), 479 deletions(-) diff --git a/agent-client-protocol-schema/src/v1/client.rs b/agent-client-protocol-schema/src/v1/client.rs index 00cdeee9e..ddf106e9f 100644 --- a/agent-client-protocol-schema/src/v1/client.rs +++ b/agent-client-protocol-schema/src/v1/client.rs @@ -185,6 +185,9 @@ pub enum SessionUpdate { /// This capability is not part of the spec yet, and may be removed or changed at any point. /// /// A streamed content block of an inter-session message. +/// +/// A chunk carries only content. The message's participants and references come from +/// `session_message` upserts, which can arrive before, between, or after its chunks. #[cfg(feature = "unstable_subagents")] #[serde_as] #[skip_serializing_none] @@ -195,34 +198,6 @@ pub enum SessionUpdate { pub struct SessionMessageChunk { /// Identifier of this message within the enclosing session's transcript. pub message_id: MessageId, - /// Optional sending session identity; omission or `null` retains a known value. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub sender_session_id: Option, - /// Optional receiving session identity; omission or `null` retains a known value. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub recipient_session_id: Option, - /// On an incoming view, the `messageId` of the same message's outgoing view in the - /// sender's transcript. - /// - /// Optional and nullable: omission or `null` means not supplied and retains a known - /// value, so it can arrive after the message starts streaming. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub sender_message_id: Option, - /// On an outgoing view, the `messageId` of the same message's incoming view in the - /// recipient's transcript. - /// - /// Optional and nullable: omission or `null` means not supplied and retains a known - /// value, so it can arrive after the message starts streaming. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub recipient_message_id: Option, /// A single content block appended to the message. pub content: ContentBlock, /// Optional and nullable chunk-scoped metadata; omitted or `null` means none. @@ -241,49 +216,11 @@ impl SessionMessageChunk { pub fn new(message_id: impl Into, content: ContentBlock) -> Self { Self { message_id: message_id.into(), - sender_session_id: None, - recipient_session_id: None, - sender_message_id: None, - recipient_message_id: None, content, meta: None, } } - /// Supplies the sending session identity, when known. - #[must_use] - pub fn sender_session_id(mut self, sender_session_id: impl IntoOption) -> Self { - self.sender_session_id = sender_session_id.into_option(); - self - } - - /// Supplies the receiving session identity, when known. - #[must_use] - pub fn recipient_session_id( - mut self, - recipient_session_id: impl IntoOption, - ) -> Self { - self.recipient_session_id = recipient_session_id.into_option(); - self - } - - /// Links an incoming view to the outgoing view's message ID in the sender's transcript. - #[must_use] - pub fn sender_message_id(mut self, sender_message_id: impl IntoOption) -> Self { - self.sender_message_id = sender_message_id.into_option(); - self - } - - /// Links an outgoing view to the incoming view's message ID in the recipient's transcript. - #[must_use] - pub fn recipient_message_id( - mut self, - recipient_message_id: impl IntoOption, - ) -> Self { - self.recipient_message_id = recipient_message_id.into_option(); - self - } - /// Sets optional chunk-scoped metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { @@ -540,7 +477,6 @@ mod session_message_tests { for (transcript, id) in [("parent", "sent-1"), ("child", "received-9")] { let wire = json!({"sessionId": transcript, "update": { "sessionUpdate": "session_message_chunk", "messageId": id, - "senderSessionId": "parent", "recipientSessionId": "child", "content": {"type": "text", "text": "first"} }}); let decoded: SessionNotification = serde_json::from_value(wire.clone()).unwrap(); @@ -551,12 +487,8 @@ mod session_message_tests { let chunk = SessionMessageChunk::new( id, ContentBlock::Text(crate::v1::TextContent::new("first")), - ) - .sender_session_id(SessionId::new("parent")) - .recipient_session_id(SessionId::new("child")); + ); assert_eq!(chunk.message_id, upsert.message_id); - assert_eq!(chunk.sender_session_id, upsert.sender_session_id); - assert_eq!(chunk.recipient_session_id, upsert.recipient_session_id); assert_eq!( serde_json::to_value(SessionUpdate::SessionMessageChunk(chunk)).unwrap(), wire["update"] @@ -564,7 +496,6 @@ mod session_message_tests { } let wire = json!({"sessionId": "child", "update": { "sessionUpdate": "session_message_chunk", "messageId": "received-9", - "senderSessionId": "parent", "recipientSessionId": "child", "content": {"type": "text", "text": "first"} }}); let base = wire["update"].clone(); @@ -601,6 +532,17 @@ mod session_message_tests { assert_eq!(encoded, value); } } + // A chunk carries only content, so identity fields on one are not part of it. + let with_identity = json!({"sessionUpdate": "session_message_chunk", "messageId": "m1", + "senderSessionId": "parent", "recipientSessionId": "child", + "senderMessageId": "sent-1", "recipientMessageId": "received-1", + "content": {"type": "text", "text": "hello"}}); + let decoded: SessionUpdate = serde_json::from_value(with_identity).unwrap(); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + json!({"sessionUpdate": "session_message_chunk", "messageId": "m1", + "content": {"type": "text", "text": "hello"}}) + ); } #[cfg(feature = "schemars")] @@ -634,15 +576,16 @@ mod session_message_tests { .recipient_session_id(SessionId::new("child")); assert_eq!(enriched.sender_session_id, Some(SessionId::new("parent"))); assert_eq!(enriched.recipient_session_id, Some(SessionId::new("child"))); - let later = - SessionMessageChunk::new("m1", block).sender_session_id(SessionId::new("parent")); + let later = SessionMessage::new("m1").sender_session_id(SessionId::new("parent")); assert_eq!(later.sender_session_id, Some(SessionId::new("parent"))); assert_eq!(later.recipient_session_id, None); + let more = SessionMessageChunk::new("m1", block); for update in [ SessionUpdate::SessionMessage(minimal), SessionUpdate::SessionMessageChunk(first), SessionUpdate::SessionMessage(enriched), - SessionUpdate::SessionMessageChunk(later), + SessionUpdate::SessionMessage(later), + SessionUpdate::SessionMessageChunk(more), ] { let wire = serde_json::to_value(&update).unwrap(); let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); @@ -652,46 +595,37 @@ mod session_message_tests { #[test] fn invalid_or_null_endpoints_are_absent_but_message_id_is_required() { - for (kind, content) in [ - ("session_message", None), - ( - "session_message_chunk", - Some(json!({"type": "text", "text": "hello"})), - ), - ] { - let mut base = json!({"sessionUpdate": kind, "messageId": "m1", - "senderSessionId": null, "recipientSessionId": 42}); - if let Some(content) = content { - base["content"] = content; - } - let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap(); - let encoded = serde_json::to_value(decoded).unwrap(); - base.as_object_mut().unwrap().remove("senderSessionId"); - base.as_object_mut().unwrap().remove("recipientSessionId"); - assert_eq!(encoded, base); - for bad in [Value::Null, json!(42)] { - let mut invalid = base.clone(); - invalid["messageId"] = bad; - assert!(serde_json::from_value::(invalid).is_err()); - } + let mut base = json!({"sessionUpdate": "session_message", "messageId": "m1", + "senderSessionId": null, "recipientSessionId": 42}); + let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap(); + let encoded = serde_json::to_value(decoded).unwrap(); + base.as_object_mut().unwrap().remove("senderSessionId"); + base.as_object_mut().unwrap().remove("recipientSessionId"); + assert_eq!(encoded, base); + for bad in [Value::Null, json!(42)] { + let mut invalid = base.clone(); + invalid["messageId"] = bad; + assert!(serde_json::from_value::(invalid).is_err()); } } #[test] fn message_references_link_views_and_can_arrive_late() { let block = ContentBlock::Text(crate::v1::TextContent::new("hello")); - let incoming = SessionMessageChunk::new("received-1", block) + let incoming = SessionMessage::new("received-1") .sender_session_id(SessionId::new("parent")) .recipient_session_id(SessionId::new("child")) - .sender_message_id(MessageId::new("sent-1")); + .sender_message_id(MessageId::new("sent-1")) + .content(vec![block]); + // The outgoing view gets its reference after it started, with a metadata-only upsert. let outgoing = SessionMessage::new("sent-1").recipient_message_id(MessageId::new("received-1")); for (update, wire) in [ ( - SessionUpdate::SessionMessageChunk(incoming), - json!({"sessionUpdate": "session_message_chunk", "messageId": "received-1", + SessionUpdate::SessionMessage(incoming), + json!({"sessionUpdate": "session_message", "messageId": "received-1", "senderSessionId": "parent", "recipientSessionId": "child", - "senderMessageId": "sent-1", "content": {"type": "text", "text": "hello"}}), + "senderMessageId": "sent-1", "content": [{"type": "text", "text": "hello"}]}), ), ( SessionUpdate::SessionMessage(outgoing), @@ -704,27 +638,16 @@ mod session_message_tests { assert_eq!(decoded, update); } - for (kind, content) in [ - ("session_message", None), - ( - "session_message_chunk", - Some(json!({"type": "text", "text": "hello"})), - ), - ] { - let mut wire = json!({"sessionUpdate": kind, "messageId": "m1", - "senderMessageId": null, "recipientMessageId": 42}); - if let Some(content) = content { - wire["content"] = content; - } - let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); - wire.as_object_mut().unwrap().remove("senderMessageId"); - wire.as_object_mut().unwrap().remove("recipientMessageId"); - assert_eq!( - serde_json::to_value(decoded).unwrap(), - wire, - "{kind}: null or malformed references mean not supplied" - ); - } + let mut wire = json!({"sessionUpdate": "session_message", "messageId": "m1", + "senderMessageId": null, "recipientMessageId": 42}); + let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); + wire.as_object_mut().unwrap().remove("senderMessageId"); + wire.as_object_mut().unwrap().remove("recipientMessageId"); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + wire, + "null or malformed references mean not supplied" + ); } } diff --git a/agent-client-protocol-schema/src/v2/client.rs b/agent-client-protocol-schema/src/v2/client.rs index 8d28a0995..e894b41ec 100644 --- a/agent-client-protocol-schema/src/v2/client.rs +++ b/agent-client-protocol-schema/src/v2/client.rs @@ -195,6 +195,9 @@ pub enum SessionUpdate { /// This capability is not part of the spec yet, and may be removed or changed at any point. /// /// A streamed content block of an inter-session message. +/// +/// A chunk carries only content. The message's participants and references come from +/// `session_message` upserts, which can arrive before, between, or after its chunks. #[cfg(feature = "unstable_subagents")] #[serde_as] #[skip_serializing_none] @@ -205,34 +208,6 @@ pub enum SessionUpdate { pub struct SessionMessageChunk { /// Identifier of this message within the enclosing session's transcript. pub message_id: MessageId, - /// Optional sending session identity; omission or `null` retains a known value. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub sender_session_id: Option, - /// Optional receiving session identity; omission or `null` retains a known value. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub recipient_session_id: Option, - /// On an incoming view, the `messageId` of the same message's outgoing view in the - /// sender's transcript. - /// - /// Optional and nullable: omission or `null` means not supplied and retains a known - /// value, so it can arrive after the message starts streaming. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub sender_message_id: Option, - /// On an outgoing view, the `messageId` of the same message's incoming view in the - /// recipient's transcript. - /// - /// Optional and nullable: omission or `null` means not supplied and retains a known - /// value, so it can arrive after the message starts streaming. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - pub recipient_message_id: Option, /// A single content block appended to the message. pub content: ContentBlock, /// Optional and nullable chunk-scoped metadata; omitted or `null` means none. @@ -251,49 +226,11 @@ impl SessionMessageChunk { pub fn new(message_id: impl Into, content: ContentBlock) -> Self { Self { message_id: message_id.into(), - sender_session_id: None, - recipient_session_id: None, - sender_message_id: None, - recipient_message_id: None, content, meta: None, } } - /// Supplies the sending session identity, when known. - #[must_use] - pub fn sender_session_id(mut self, sender_session_id: impl IntoOption) -> Self { - self.sender_session_id = sender_session_id.into_option(); - self - } - - /// Supplies the receiving session identity, when known. - #[must_use] - pub fn recipient_session_id( - mut self, - recipient_session_id: impl IntoOption, - ) -> Self { - self.recipient_session_id = recipient_session_id.into_option(); - self - } - - /// Links an incoming view to the outgoing view's message ID in the sender's transcript. - #[must_use] - pub fn sender_message_id(mut self, sender_message_id: impl IntoOption) -> Self { - self.sender_message_id = sender_message_id.into_option(); - self - } - - /// Links an outgoing view to the incoming view's message ID in the recipient's transcript. - #[must_use] - pub fn recipient_message_id( - mut self, - recipient_message_id: impl IntoOption, - ) -> Self { - self.recipient_message_id = recipient_message_id.into_option(); - self - } - /// Sets optional chunk-scoped metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { @@ -466,7 +403,6 @@ mod session_message_tests { for (transcript, id) in [("parent", "sent-1"), ("child", "received-9")] { let wire = json!({"sessionId": transcript, "update": { "sessionUpdate": "session_message_chunk", "messageId": id, - "senderSessionId": "parent", "recipientSessionId": "child", "content": {"type": "text", "text": "hello"} }}); let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap(); @@ -478,18 +414,25 @@ mod session_message_tests { let chunk = SessionMessageChunk::new( id, ContentBlock::Text(crate::v2::TextContent::new("hello")), - ) - .sender_session_id(SessionId::new("parent")) - .recipient_session_id(SessionId::new("child")); + ); assert_eq!(chunk.message_id, upsert.message_id); - assert_eq!(chunk.sender_session_id, upsert.sender_session_id); - assert_eq!(chunk.recipient_session_id, upsert.recipient_session_id); assert_eq!( serde_json::to_value(SessionUpdate::SessionMessageChunk(chunk)).unwrap(), wire["update"] ); assert_eq!(serde_json::to_value(upsert).unwrap()["content"], json!([])); } + // A chunk carries only content, so identity fields on one are not part of it. + let with_identity = json!({"sessionUpdate": "session_message_chunk", "messageId": "m1", + "senderSessionId": "parent", "recipientSessionId": "child", + "senderMessageId": "sent-1", "recipientMessageId": "received-1", + "content": {"type": "text", "text": "hello"}}); + let decoded: SessionUpdate = serde_json::from_value(with_identity).unwrap(); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + json!({"sessionUpdate": "session_message_chunk", "messageId": "m1", + "content": {"type": "text", "text": "hello"}}) + ); } #[test] @@ -501,8 +444,11 @@ mod session_message_tests { json!({"type": "text", "text": "hello"}), ), ] { - let base = json!({"sessionUpdate": kind, "messageId": "m1", - "senderSessionId": "parent", "recipientSessionId": "child", "content": content}); + let mut base = json!({"sessionUpdate": kind, "messageId": "m1", "content": content}); + if kind == "session_message" { + base["senderSessionId"] = json!("parent"); + base["recipientSessionId"] = json!("child"); + } { let key = "messageId"; let mut missing = base.clone(); @@ -612,15 +558,16 @@ mod session_message_tests { .recipient_session_id(SessionId::new("child")); assert_eq!(enriched.sender_session_id, Some(SessionId::new("parent"))); assert_eq!(enriched.recipient_session_id, Some(SessionId::new("child"))); - let later = - SessionMessageChunk::new("m1", block).sender_session_id(SessionId::new("parent")); + let later = SessionMessage::new("m1").sender_session_id(SessionId::new("parent")); assert_eq!(later.sender_session_id, Some(SessionId::new("parent"))); assert_eq!(later.recipient_session_id, None); + let more = SessionMessageChunk::new("m1", block); for update in [ SessionUpdate::SessionMessage(minimal), SessionUpdate::SessionMessageChunk(first), SessionUpdate::SessionMessage(enriched), - SessionUpdate::SessionMessageChunk(later), + SessionUpdate::SessionMessage(later), + SessionUpdate::SessionMessageChunk(more), ] { let wire = serde_json::to_value(&update).unwrap(); let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); @@ -630,46 +577,37 @@ mod session_message_tests { #[test] fn invalid_or_null_endpoints_are_absent_but_message_id_is_required() { - for (kind, content) in [ - ("session_message", None), - ( - "session_message_chunk", - Some(json!({"type": "text", "text": "hello"})), - ), - ] { - let mut base = json!({"sessionUpdate": kind, "messageId": "m1", - "senderSessionId": null, "recipientSessionId": 42}); - if let Some(content) = content { - base["content"] = content; - } - let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap(); - let encoded = serde_json::to_value(decoded).unwrap(); - base.as_object_mut().unwrap().remove("senderSessionId"); - base.as_object_mut().unwrap().remove("recipientSessionId"); - assert_eq!(encoded, base); - for bad in [Value::Null, json!(42)] { - let mut invalid = base.clone(); - invalid["messageId"] = bad; - assert!(serde_json::from_value::(invalid).is_err()); - } + let mut base = json!({"sessionUpdate": "session_message", "messageId": "m1", + "senderSessionId": null, "recipientSessionId": 42}); + let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap(); + let encoded = serde_json::to_value(decoded).unwrap(); + base.as_object_mut().unwrap().remove("senderSessionId"); + base.as_object_mut().unwrap().remove("recipientSessionId"); + assert_eq!(encoded, base); + for bad in [Value::Null, json!(42)] { + let mut invalid = base.clone(); + invalid["messageId"] = bad; + assert!(serde_json::from_value::(invalid).is_err()); } } #[test] fn message_references_link_views_and_can_arrive_late() { let block = ContentBlock::Text(crate::v2::TextContent::new("hello")); - let incoming = SessionMessageChunk::new("received-1", block) + let incoming = SessionMessage::new("received-1") .sender_session_id(SessionId::new("parent")) .recipient_session_id(SessionId::new("child")) - .sender_message_id(MessageId::new("sent-1")); + .sender_message_id(MessageId::new("sent-1")) + .content(vec![block]); + // The outgoing view gets its reference after it started, with a metadata-only upsert. let outgoing = SessionMessage::new("sent-1").recipient_message_id(MessageId::new("received-1")); for (update, wire) in [ ( - SessionUpdate::SessionMessageChunk(incoming), - json!({"sessionUpdate": "session_message_chunk", "messageId": "received-1", + SessionUpdate::SessionMessage(incoming), + json!({"sessionUpdate": "session_message", "messageId": "received-1", "senderSessionId": "parent", "recipientSessionId": "child", - "senderMessageId": "sent-1", "content": {"type": "text", "text": "hello"}}), + "senderMessageId": "sent-1", "content": [{"type": "text", "text": "hello"}]}), ), ( SessionUpdate::SessionMessage(outgoing), @@ -682,27 +620,16 @@ mod session_message_tests { assert_eq!(decoded, update); } - for (kind, content) in [ - ("session_message", None), - ( - "session_message_chunk", - Some(json!({"type": "text", "text": "hello"})), - ), - ] { - let mut wire = json!({"sessionUpdate": kind, "messageId": "m1", - "senderMessageId": null, "recipientMessageId": 42}); - if let Some(content) = content { - wire["content"] = content; - } - let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); - wire.as_object_mut().unwrap().remove("senderMessageId"); - wire.as_object_mut().unwrap().remove("recipientMessageId"); - assert_eq!( - serde_json::to_value(decoded).unwrap(), - wire, - "{kind}: null or malformed references mean not supplied" - ); - } + let mut wire = json!({"sessionUpdate": "session_message", "messageId": "m1", + "senderMessageId": null, "recipientMessageId": 42}); + let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap(); + wire.as_object_mut().unwrap().remove("senderMessageId"); + wire.as_object_mut().unwrap().remove("recipientMessageId"); + assert_eq!( + serde_json::to_value(decoded).unwrap(), + wire, + "null or malformed references mean not supplied" + ); } } diff --git a/docs/protocol/v1/draft/prompt-turn.mdx b/docs/protocol/v1/draft/prompt-turn.mdx index 1c86e353a..03c734c5b 100644 --- a/docs/protocol/v1/draft/prompt-turn.mdx +++ b/docs/protocol/v1/draft/prompt-turn.mdx @@ -43,11 +43,14 @@ When the Client advertises `subagents: {}`, the Agent may report outgoing and incoming messages between sessions, separately from user-facing responses and tool calls. The outer `params.sessionId` identifies the transcript being updated. Every message and chunk requires a `messageId` scoped to that -transcript. Optional `senderSessionId` and `recipientSessionId` metadata -identify the participants. Include them on the first update when available; -later updates may omit them or add missing identities. +transcript. Optional `senderSessionId` and `recipientSessionId` on +`session_message` identify the participants; chunks carry only content. +Include them on the first `session_message` when available, before the first +chunk of a streamed message; later updates may omit them or add missing +identities. -For example, an incoming message in the child can begin with a streamed chunk: +For example, an incoming message in the child can name its participants, then +stream its content: ```json { @@ -56,10 +59,24 @@ For example, an incoming message in the child can begin with a streamed chunk: "params": { "sessionId": "child_session", "update": { - "sessionUpdate": "session_message_chunk", + "sessionUpdate": "session_message", "messageId": "received_message_1", "senderSessionId": "parent_session", - "recipientSessionId": "child_session", + "recipientSessionId": "child_session" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "method": "session/update", + "params": { + "sessionId": "child_session", + "update": { + "sessionUpdate": "session_message_chunk", + "messageId": "received_message_1", "content": { "type": "text", "text": "Please investigate " } } } @@ -80,7 +97,7 @@ participant IDs mean not supplied and leave previously known identities intact. The two views have independent message IDs. Optional `senderMessageId` on the incoming view and `recipientMessageId` on the outgoing view name the other view's `messageId`, so a Client can go from either view to the other. They -follow the participant-ID rules and can arrive in a later update; Clients must +follow the participant-ID rules and can arrive in a later `session_message`; Clients must not link views by equal IDs alone. The Agent reports each side only when observed; Clients must not synthesize an incoming entry from an outgoing one or forward the content themselves. Neither view acknowledges completed diff --git a/docs/protocol/v1/draft/schema.mdx b/docs/protocol/v1/draft/schema.mdx index 5668fdf35..6e57a0328 100644 --- a/docs/protocol/v1/draft/schema.mdx +++ b/docs/protocol/v1/draft/schema.mdx @@ -6491,6 +6491,9 @@ This capability is not part of the spec yet, and may be removed or changed at an A streamed content block of an inter-session message. +A chunk carries only content. The message's participants and references come from +`session_message` upserts, which can arrive before, between, or after its chunks. + **Type:** Object **Properties:** @@ -6507,28 +6510,6 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. -MessageId | null} > - On an outgoing view, the `messageId` of the same message's incoming view in the -recipient's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional receiving session identity; omission or `null` retains a known value. - -MessageId | null} > - On an incoming view, the `messageId` of the same message's outgoing view in the -sender's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional sending session identity; omission or `null` retains a known value. - ## SessionMode @@ -7361,28 +7342,6 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. -MessageId | null} > - On an outgoing view, the `messageId` of the same message's incoming view in the -recipient's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional receiving session identity; omission or `null` retains a known value. - -MessageId | null} > - On an incoming view, the `messageId` of the same message's outgoing view in the -sender's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional sending session identity; omission or `null` retains a known value. - The discriminator value. Must be `"session_message_chunk"`. diff --git a/docs/protocol/v2/draft/prompt-lifecycle.mdx b/docs/protocol/v2/draft/prompt-lifecycle.mdx index 3ebe91aa5..6bdbe36ef 100644 --- a/docs/protocol/v2/draft/prompt-lifecycle.mdx +++ b/docs/protocol/v2/draft/prompt-lifecycle.mdx @@ -73,11 +73,14 @@ The Agent may report outgoing and incoming messages between sessions, separately from user-facing responses and tool calls. The outer `params.sessionId` identifies the transcript being updated. Every `session_message` or `session_message_chunk` requires `messageId`, scoped to -that transcript. Optional `senderSessionId` and `recipientSessionId` metadata -identify the participants. Include them on the first update when available; -later updates may omit them or add missing identities. +that transcript. Optional `senderSessionId` and `recipientSessionId` on +`session_message` identify the participants; chunks carry only content, as +`agent_message_chunk` does. Include them on the first `session_message` when +available, before the first chunk of a streamed message; later updates may +omit them or add missing identities. -For example, an incoming message in the child can begin with a streamed chunk: +For example, an incoming message in the child can name its participants, then +stream its content: ```json { @@ -86,10 +89,24 @@ For example, an incoming message in the child can begin with a streamed chunk: "params": { "sessionId": "child_session", "update": { - "sessionUpdate": "session_message_chunk", + "sessionUpdate": "session_message", "messageId": "received_message_1", "senderSessionId": "parent_session", - "recipientSessionId": "child_session", + "recipientSessionId": "child_session" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "method": "session/update", + "params": { + "sessionId": "child_session", + "update": { + "sessionUpdate": "session_message_chunk", + "messageId": "received_message_1", "content": { "type": "text", "text": "Please investigate " } } } @@ -111,7 +128,7 @@ participant IDs mean not supplied and leave previously known identities intact. The two views have independent message IDs. Optional `senderMessageId` on the incoming view and `recipientMessageId` on the outgoing view name the other view's `messageId`, so a Client can go from either view to the other. They -follow the participant-ID rules and can arrive in a later update; Clients must +follow the participant-ID rules and can arrive in a later `session_message`; Clients must not link views by equal IDs alone. The Agent reports each side only when observed; Clients must not synthesize an incoming entry from an outgoing one or forward the content themselves. Neither view acknowledges completed diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index 709250bb5..0ec76b829 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -6800,6 +6800,9 @@ This capability is not part of the spec yet, and may be removed or changed at an A streamed content block of an inter-session message. +A chunk carries only content. The message's participants and references come from +`session_message` upserts, which can arrive before, between, or after its chunks. + **Type:** Object **Properties:** @@ -6816,28 +6819,6 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. -MessageId | null} > - On an outgoing view, the `messageId` of the same message's incoming view in the -recipient's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional receiving session identity; omission or `null` retains a known value. - -MessageId | null} > - On an incoming view, the `messageId` of the same message's outgoing view in the -sender's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional sending session identity; omission or `null` retains a known value. - ## SessionSystemPromptCapabilities @@ -7941,28 +7922,6 @@ Implementations MUST NOT make assumptions about values in `_meta`. MessageId} required> Identifier of this message within the enclosing session's transcript. -MessageId | null} > - On an outgoing view, the `messageId` of the same message's incoming view in the -recipient's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional receiving session identity; omission or `null` retains a known value. - -MessageId | null} > - On an incoming view, the `messageId` of the same message's outgoing view in the -sender's transcript. - -Optional and nullable: omission or `null` means not supplied and retains a known -value, so it can arrive after the message starts streaming. - - -SessionId | null} > - Optional sending session identity; omission or `null` retains a known value. - The discriminator value. Must be `"session_message_chunk"`. diff --git a/docs/rfds/subagents.mdx b/docs/rfds/subagents.mdx index 2c1566287..cbaa49422 100644 --- a/docs/rfds/subagents.mdx +++ b/docs/rfds/subagents.mdx @@ -16,9 +16,10 @@ session. The Agent associates a reusable child session with its parent through an upsert-style `subagent_update`. Child events then flow automatically on the -same connection. Messages between sessions use `session_message` upserts or -`session_message_chunk` notifications with normal message content and optional -sender and recipient metadata, rather than tool-call content. +same connection. Messages between sessions use `session_message` upserts, +which carry optional sender and recipient metadata, and +`session_message_chunk` notifications with normal message content, rather +than tool-call content. The association is not a task or a one-shot lifetime. The parent can message the same child again after earlier work finishes. Client-initiated session @@ -257,11 +258,20 @@ calls. They use the existing `session/update` notification, not a new JSON-RPC method or a new `ContentBlock` type. The enclosing `params.sessionId` identifies the transcript being updated, not -necessarily the sender. Each update has: +necessarily the sender. Both variants have: - `messageId`: required, non-nullable, and unique within the enclosing session, just like other message IDs. It identifies the entry in that transcript, not a globally shared message or either participant. +- `content`: a `session_message` may replace the whole `ContentBlock[]`; + `session_message_chunk` requires one non-nullable `ContentBlock` to append. + Normal text, images, audio, and resource content use their existing shapes. +- `_meta`: optional and nullable. It updates message-scoped metadata on + `session_message`; it is chunk-scoped on `session_message_chunk`. + +Only `session_message` carries the message's identity. A chunk carries only +content, as `agent_message_chunk` does: + - `senderSessionId`: optional and nullable. Identifies the sending session when supplied. - `recipientSessionId`: optional and nullable. Identifies the receiving session @@ -271,17 +281,14 @@ necessarily the sender. Each update has: - `recipientMessageId`: optional and nullable. On an outgoing view, the `messageId` of the same message's incoming view in the recipient's transcript. -- `content`: a `session_message` may replace the whole `ContentBlock[]`; - `session_message_chunk` requires one non-nullable `ContentBlock` to append. - Normal text, images, audio, and resource content use their existing shapes. -- `_meta`: optional and nullable. It updates message-scoped metadata on - `session_message`; it is chunk-scoped on `session_message_chunk`. -Agents **SHOULD** include participant IDs on the first message update or chunk -when available. Later events may omit them or supply previously missing -identities. For these identity fields, omission and `null` mean not supplied: -the Client retains any previously reported value. They do not use the -null-clearing semantics of the mutable `content` and `_meta` fields. +Agents **SHOULD** include participant IDs on the first `session_message` for a +message when available, and when streaming a message whose participants are +known, send that upsert before its first chunk. Later upserts may omit them or +supply previously missing identities. For these identity fields, omission and +`null` mean not supplied: the Client retains any previously reported value. +They do not use the null-clearing semantics of the mutable `content` and +`_meta` fields. `senderMessageId` and `recipientMessageId` link the two views of a message so that a Client looking at either one can find the other, including when it has @@ -289,7 +296,7 @@ not yet received or rendered the other transcript's entry. They follow the same convention as the participant IDs: omission and `null` mean not supplied, the Client retains any previously reported value, and a known value does not change. An Agent can therefore stream a view before it knows the other one's -ID and supply the reference later with a chunk or a metadata-only upsert. For +ID and supply the reference later with a metadata-only upsert. For example, it can add `recipientMessageId` to the outgoing view once it has reported the incoming view. Agents **SHOULD** supply each reference once they know it. When both views carry references, they **MUST** name each other; a @@ -442,8 +449,9 @@ notification and complete participant metadata are not required: ``` Subsequent chunks append normal message content for the same transcript-local -`messageId`. Participant metadata can arrive with a later chunk or a -metadata-only upsert, without repeating or replacing the content: +`messageId`. Participant metadata and references arrive in a metadata-only +upsert, before, between, or after the chunks, without repeating or replacing +the content: ```json { @@ -464,19 +472,20 @@ metadata-only upsert, without repeating or replacing the content: A `session_message` is an upsert: when it supplies a content array, that array replaces all content accumulated for the message rather than appending. It can also update metadata without resending content. Later chunks append to the -stored content; chunk metadata does not patch message metadata. +stored content. A chunk carries no participant IDs or references, and its +`_meta` does not patch message metadata. ### Why session IDs appear in different places These IDs answer different questions; they are not competing ways to route the same event: -| Location | Purpose | -| ---------------------------------------------------------------- | ---------------------------------------------------------------- | -| Outer `params.sessionId` | Which session's transcript or entities does this update address? | -| `params.update.sessionId` in `subagent_update` | Which child is owned by the parent named by the outer ID? | -| `params.update.senderSessionId` in a session message or chunk | Which session sent, or is sending, this message? | -| `params.update.recipientSessionId` in a session message or chunk | Which session is this message addressed to? | +| Location | Purpose | +| ------------------------------------------------------- | ---------------------------------------------------------------- | +| Outer `params.sessionId` | Which session's transcript or entities does this update address? | +| `params.update.sessionId` in `subagent_update` | Which child is owned by the parent named by the outer ID? | +| `params.update.senderSessionId` in a session message | Which session sent, or is sending, this message? | +| `params.update.recipientSessionId` in a session message | Which session is this message addressed to? | For example, with parent `sess_parent` and child `sess_child_1`: @@ -980,8 +989,9 @@ implementation to use the named SDKs, providers, or integration techniques. 1. Add the v1 `SubagentCapabilities` marker and the `SubagentUpdate` association and `SubagentSessionCapabilities` types in both versions. 2. Add `SessionMessage` and `SessionMessageChunk` update variants in both - versions, using normal content and optional sender and recipient session - metadata for incoming and outgoing views. + versions, using normal content, with optional sender and recipient session + metadata and message references on `SessionMessage` for incoming and + outgoing views. 3. Mirror the v2 `StateUpdate` payload in v1 and embed it in the v1 association update. In v2, mirror ordinary child-session state notifications in the parent association as well. @@ -1058,13 +1068,15 @@ implementation of child routing or the replay freshness boundary. communication as an ordinary user message. - Stream an incoming view before its outgoing view's ID is known, then supply `senderMessageId` on the incoming view and `recipientMessageId` on the - outgoing view. Verify that a Client can go from either view to the other, + outgoing view with metadata-only upserts. Verify that a Client can go from + either view to the other, including before it has rendered the other transcript, and that it does not link views by equal IDs alone. - Start a directed message with a chunk, interleave another message, and replace accumulated content with a whole snapshot. Start without participant - metadata, add it later without replacing content, and omit it on subsequent - chunks. Verify that missing metadata remains an inter-session message rather + metadata, add it later with a metadata-only upsert without replacing content, + and keep streaming chunks. Verify that missing metadata remains an + inter-session message rather than being attributed to a human or guessed sender. - Finish reporting a message before the child finishes processing it, and keep receiving child events after the parent's prompt response. @@ -1216,6 +1228,16 @@ before it knows the other one's ID. Message IDs stay scoped to their transcript; requiring both views to share an ID would instead force the Agent to know the link before sending the first update of either view. +### Why do chunks carry only content? + +A message's identity, its participants and references, lives in one place: +the `session_message` upsert, as message-level fields do for +`agent_message`, whose chunks also carry only content. Identity on chunks +would give the same fields two sources that can disagree, and would make a +chunk patch message state even though its `_meta` does not. An Agent that +learns the identity while streaming sends one metadata-only upsert, which +needs no content. + ### Why not allow prompting or steering a subagent? The parent Agent can message or reuse a child according to its runtime's @@ -1271,7 +1293,10 @@ important difference from a user-facing session. - 2026-10-22: Added optional `senderMessageId` on the incoming view and `recipientMessageId` on the outgoing view of a session-directed message, so a Client can go from either view to the other without inferring a link from - equal IDs. Either can be supplied after the view starts streaming. + equal IDs. Either can be supplied after the view starts streaming. Moved the + participant IDs off `session_message_chunk`: chunks carry only content, and + a message's participants and references arrive on `session_message` + upserts. - 2026-10-13: In v2, a concrete top-level `_meta` object on `subagent_update` and `session_message` patches stored metadata by key, per [v2 Metadata Patches](/rfds/v2/meta-patches). v1 still replaces the whole diff --git a/schema/v1/schema.unstable.json b/schema/v1/schema.unstable.json index 6c0771882..d49f8b30c 100644 --- a/schema/v1/schema.unstable.json +++ b/schema/v1/schema.unstable.json @@ -5738,7 +5738,7 @@ "required": ["messageId"] }, "SessionMessageChunk": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA streamed content block of an inter-session message.", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA streamed content block of an inter-session message.\n\nA chunk carries only content. The message's participants and references come from\n`session_message` upserts, which can arrive before, between, or after its chunks.", "type": "object", "properties": { "messageId": { @@ -5749,54 +5749,6 @@ } ] }, - "senderSessionId": { - "description": "Optional sending session identity; omission or `null` retains a known value.", - "anyOf": [ - { - "$ref": "#/$defs/SessionId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, - "recipientSessionId": { - "description": "Optional receiving session identity; omission or `null` retains a known value.", - "anyOf": [ - { - "$ref": "#/$defs/SessionId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, - "senderMessageId": { - "description": "On an incoming view, the `messageId` of the same message's outgoing view in the\nsender's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", - "anyOf": [ - { - "$ref": "#/$defs/MessageId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, - "recipientMessageId": { - "description": "On an outgoing view, the `messageId` of the same message's incoming view in the\nrecipient's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", - "anyOf": [ - { - "$ref": "#/$defs/MessageId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, "content": { "description": "A single content block appended to the message.", "allOf": [ diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 2cedde5bb..8d0c422ad 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -7000,7 +7000,7 @@ "required": ["messageId"] }, "SessionMessageChunk": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA streamed content block of an inter-session message.", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA streamed content block of an inter-session message.\n\nA chunk carries only content. The message's participants and references come from\n`session_message` upserts, which can arrive before, between, or after its chunks.", "type": "object", "properties": { "messageId": { @@ -7011,54 +7011,6 @@ } ] }, - "senderSessionId": { - "description": "Optional sending session identity; omission or `null` retains a known value.", - "anyOf": [ - { - "$ref": "#/$defs/SessionId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, - "recipientSessionId": { - "description": "Optional receiving session identity; omission or `null` retains a known value.", - "anyOf": [ - { - "$ref": "#/$defs/SessionId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, - "senderMessageId": { - "description": "On an incoming view, the `messageId` of the same message's outgoing view in the\nsender's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", - "anyOf": [ - { - "$ref": "#/$defs/MessageId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, - "recipientMessageId": { - "description": "On an outgoing view, the `messageId` of the same message's incoming view in the\nrecipient's transcript.\n\nOptional and nullable: omission or `null` means not supplied and retains a known\nvalue, so it can arrive after the message starts streaming.", - "anyOf": [ - { - "$ref": "#/$defs/MessageId" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": true - }, "content": { "description": "A single content block appended to the message.", "allOf": [