1use std::{collections::BTreeMap, sync::Arc};
7
8use derive_more::{Display, From};
9#[cfg(feature = "schemars")]
10use schemars::Schema;
11use serde::{Deserialize, Serialize};
12use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
13
14#[cfg(feature = "unstable_plan_operations")]
15use super::PlanRemoved;
16#[cfg(feature = "unstable_end_turn_token_usage")]
17use super::Usage;
18use super::{
19 AbsolutePath, ContentBlock, ExtNotification, ExtRequest, ExtResponse, Meta, PlanUpdate,
20 SessionConfigOption, SessionId, StopReason, TerminalId, TerminalOutputChunk, TerminalUpdate,
21 ToolCallContentChunk, ToolCallId, ToolCallUpdate,
22};
23use super::{
24 CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse,
25 ElicitationCapabilities,
26};
27use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined};
28
29#[cfg(feature = "unstable_mcp_over_acp")]
30use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse};
31
32#[serde_as]
40#[skip_serializing_none]
41#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION)))]
44#[serde(rename_all = "camelCase")]
45#[non_exhaustive]
46pub struct UpdateSessionNotification {
47 pub session_id: SessionId,
49 pub update: SessionUpdate,
51 #[serde_as(deserialize_as = "DefaultOnError")]
57 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
58 #[serde(default)]
59 #[serde(rename = "_meta")]
60 pub meta: Option<Meta>,
61}
62
63impl UpdateSessionNotification {
64 #[must_use]
66 pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
67 Self {
68 session_id: session_id.into(),
69 update,
70 meta: None,
71 }
72 }
73
74 #[must_use]
80 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
81 self.meta = meta.into_option();
82 self
83 }
84}
85
86#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
94#[non_exhaustive]
95pub enum SessionUpdate {
96 UserMessageChunk(ContentChunk),
98 UserMessage(UserMessage),
104 AgentMessageChunk(ContentChunk),
106 AgentMessage(AgentMessage),
112 AgentThoughtChunk(ContentChunk),
114 AgentThought(AgentThought),
120 StateUpdate(StateUpdate),
122 ToolCallContentChunk(ToolCallContentChunk),
124 ToolCallUpdate(ToolCallUpdate),
126 TerminalUpdate(TerminalUpdate),
128 TerminalOutputChunk(TerminalOutputChunk),
130 PlanUpdate(PlanUpdate),
133 #[cfg(feature = "unstable_plan_operations")]
139 PlanRemoved(PlanRemoved),
140 AvailableCommandsUpdate(AvailableCommandsUpdate),
142 ConfigOptionUpdate(ConfigOptionUpdate),
144 SessionInfoUpdate(SessionInfoUpdate),
146 UsageUpdate(UsageUpdate),
148 Notice(Notice),
153 CompactionUpdate(CompactionUpdate),
155 CompactionSummaryChunk(CompactionSummaryChunk),
157 #[cfg(feature = "unstable_subagents")]
164 SubagentUpdate(SubagentUpdate),
165 #[cfg(feature = "unstable_subagents")]
172 SessionMessage(SessionMessage),
173 #[cfg(feature = "unstable_subagents")]
179 SessionMessageChunk(SessionMessageChunk),
180 #[serde(untagged)]
190 Other(OtherSessionUpdate),
191}
192
193#[cfg(feature = "unstable_subagents")]
199#[serde_as]
200#[skip_serializing_none]
201#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
203#[serde(rename_all = "camelCase")]
204#[non_exhaustive]
205pub struct SessionMessageChunk {
206 pub message_id: MessageId,
208 #[serde_as(deserialize_as = "DefaultOnError")]
210 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
211 #[serde(default)]
212 pub sender_session_id: Option<SessionId>,
213 #[serde_as(deserialize_as = "DefaultOnError")]
215 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
216 #[serde(default)]
217 pub recipient_session_id: Option<SessionId>,
218 pub content: ContentBlock,
220 #[serde_as(deserialize_as = "DefaultOnError")]
224 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
225 #[serde(default, rename = "_meta")]
226 pub meta: Option<Meta>,
227}
228
229#[cfg(feature = "unstable_subagents")]
230impl SessionMessageChunk {
231 #[must_use]
233 pub fn new(message_id: impl Into<MessageId>, content: ContentBlock) -> Self {
234 Self {
235 message_id: message_id.into(),
236 sender_session_id: None,
237 recipient_session_id: None,
238 content,
239 meta: None,
240 }
241 }
242
243 #[must_use]
245 pub fn sender_session_id(mut self, sender_session_id: impl IntoOption<SessionId>) -> Self {
246 self.sender_session_id = sender_session_id.into_option();
247 self
248 }
249
250 #[must_use]
252 pub fn recipient_session_id(
253 mut self,
254 recipient_session_id: impl IntoOption<SessionId>,
255 ) -> Self {
256 self.recipient_session_id = recipient_session_id.into_option();
257 self
258 }
259
260 #[must_use]
262 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
263 self.meta = meta.into_option();
264 self
265 }
266}
267
268#[cfg(feature = "unstable_subagents")]
274#[serde_as]
275#[skip_serializing_none]
276#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
278#[serde(rename_all = "camelCase")]
279#[non_exhaustive]
280pub struct SessionMessage {
281 pub message_id: MessageId,
283 #[serde_as(deserialize_as = "DefaultOnError")]
285 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
286 #[serde(default)]
287 pub sender_session_id: Option<SessionId>,
288 #[serde_as(deserialize_as = "DefaultOnError")]
290 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
291 #[serde(default)]
292 pub recipient_session_id: Option<SessionId>,
293 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
296 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
297 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
298 pub content: MaybeUndefined<Vec<ContentBlock>>,
299 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
308 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
309 #[serde(
310 default,
311 rename = "_meta",
312 skip_serializing_if = "MaybeUndefined::is_undefined"
313 )]
314 pub meta: MaybeUndefined<Meta>,
315}
316
317#[cfg(feature = "unstable_subagents")]
318impl SessionMessage {
319 #[must_use]
321 pub fn new(message_id: impl Into<MessageId>) -> Self {
322 Self {
323 message_id: message_id.into(),
324 sender_session_id: None,
325 recipient_session_id: None,
326 content: MaybeUndefined::Undefined,
327 meta: MaybeUndefined::Undefined,
328 }
329 }
330
331 #[must_use]
333 pub fn sender_session_id(mut self, sender_session_id: impl IntoOption<SessionId>) -> Self {
334 self.sender_session_id = sender_session_id.into_option();
335 self
336 }
337
338 #[must_use]
340 pub fn recipient_session_id(
341 mut self,
342 recipient_session_id: impl IntoOption<SessionId>,
343 ) -> Self {
344 self.recipient_session_id = recipient_session_id.into_option();
345 self
346 }
347
348 #[must_use]
350 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
351 self.content = content.into_maybe_undefined();
352 self
353 }
354
355 #[must_use]
360 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
361 self.meta = meta.into_maybe_undefined();
362 self
363 }
364}
365
366#[cfg(all(test, feature = "unstable_subagents"))]
367mod session_message_tests {
368 use super::*;
369 use serde_json::{Value, json};
370
371 #[test]
372 fn transcript_envelopes_and_multimodal_content() {
373 for (transcript, sender, recipient, id) in [
374 ("parent", "parent", "child", "sent-1"),
375 ("child", "parent", "child", "received-9"),
376 ("child", "child", "parent", "sent-2"),
377 ] {
378 let wire = json!({"sessionId": transcript, "update": {
379 "sessionUpdate": "session_message", "messageId": id,
380 "senderSessionId": sender, "recipientSessionId": recipient, "content": [
381 {"type": "text", "text": "hello"},
382 {"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"}
383 ]
384 }});
385 let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
386 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
387 }
388 }
389
390 #[test]
391 fn chunks_and_upserts_share_transcript_local_identity() {
392 for (transcript, id) in [("parent", "sent-1"), ("child", "received-9")] {
393 let wire = json!({"sessionId": transcript, "update": {
394 "sessionUpdate": "session_message_chunk", "messageId": id,
395 "senderSessionId": "parent", "recipientSessionId": "child",
396 "content": {"type": "text", "text": "hello"}
397 }});
398 let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
399 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
400 let upsert = SessionMessage::new(id)
401 .sender_session_id(SessionId::new("parent"))
402 .recipient_session_id(SessionId::new("child"))
403 .content(vec![]);
404 let chunk = SessionMessageChunk::new(
405 id,
406 ContentBlock::Text(crate::v2::TextContent::new("hello")),
407 )
408 .sender_session_id(SessionId::new("parent"))
409 .recipient_session_id(SessionId::new("child"));
410 assert_eq!(chunk.message_id, upsert.message_id);
411 assert_eq!(chunk.sender_session_id, upsert.sender_session_id);
412 assert_eq!(chunk.recipient_session_id, upsert.recipient_session_id);
413 assert_eq!(
414 serde_json::to_value(SessionUpdate::SessionMessageChunk(chunk)).unwrap(),
415 wire["update"]
416 );
417 assert_eq!(serde_json::to_value(upsert).unwrap()["content"], json!([]));
418 }
419 }
420
421 #[test]
422 fn required_ids_and_chunk_content_with_upsert_patch_semantics() {
423 for (kind, content) in [
424 ("session_message", json!([])),
425 (
426 "session_message_chunk",
427 json!({"type": "text", "text": "hello"}),
428 ),
429 ] {
430 let base = json!({"sessionUpdate": kind, "messageId": "m1",
431 "senderSessionId": "parent", "recipientSessionId": "child", "content": content});
432 {
433 let key = "messageId";
434 let mut missing = base.clone();
435 missing.as_object_mut().unwrap().remove(key);
436 assert!(
437 serde_json::from_value::<SessionUpdate>(missing).is_err(),
438 "{kind} {key}"
439 );
440 let mut null = base.clone();
441 null[key] = Value::Null;
442 assert!(
443 serde_json::from_value::<SessionUpdate>(null).is_err(),
444 "{kind} {key}"
445 );
446 let mut non_string = base.clone();
447 non_string[key] = json!(42);
448 assert!(
449 serde_json::from_value::<SessionUpdate>(non_string).is_err(),
450 "{kind} {key}"
451 );
452 }
453 if kind == "session_message_chunk" {
454 for content in [None, Some(Value::Null), Some(json!([]))] {
455 let mut invalid = base.clone();
456 invalid.as_object_mut().unwrap().remove("content");
457 if let Some(content) = content {
458 invalid["content"] = content;
459 }
460 assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
461 }
462 } else {
463 for content in [None, Some(Value::Null), Some(json!([])), Some(json!({}))] {
464 let mut wire = base.clone();
465 wire.as_object_mut().unwrap().remove("content");
466 if let Some(content) = content {
467 wire["content"] = content;
468 }
469 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
470 let encoded = serde_json::to_value(decoded).unwrap();
471 if wire["content"].is_object() {
472 wire.as_object_mut().unwrap().remove("content");
473 }
474 assert_eq!(encoded, wire);
475 }
476 }
477 for meta in [None, Some(Value::Null), Some(json!({"tag": "value"}))] {
478 let mut wire = base.clone();
479 if let Some(meta) = meta {
480 wire["_meta"] = meta;
481 }
482 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
483 let encoded = serde_json::to_value(decoded).unwrap();
484 if kind == "session_message" {
485 assert_eq!(encoded, wire);
486 } else if wire["_meta"].is_null() {
487 assert_eq!(encoded, base);
488 } else {
489 assert_eq!(encoded, wire);
490 }
491 }
492 }
493 let metadata_only = json!({"sessionUpdate": "session_message",
494 "messageId": "m1", "senderSessionId": "parent",
495 "recipientSessionId": "child", "_meta": {"tag": "value"}});
496 let decoded: SessionUpdate = serde_json::from_value(metadata_only.clone()).unwrap();
497 assert_eq!(serde_json::to_value(decoded).unwrap(), metadata_only);
498 let cleared = SessionMessage::new("m1")
499 .sender_session_id(SessionId::new("parent"))
500 .recipient_session_id(SessionId::new("child"))
501 .content(None)
502 .meta(None);
503 assert_eq!(
504 serde_json::to_value(cleared).unwrap(),
505 json!({"messageId": "m1", "senderSessionId": "parent",
506 "recipientSessionId": "child", "content": null, "_meta": null})
507 );
508 }
509
510 #[cfg(feature = "schemars")]
511 #[test]
512 fn schemas_require_ids_and_chunk_content_but_not_upsert_content() {
513 let upsert = serde_json::to_value(schemars::schema_for!(SessionMessage)).unwrap();
514 let required = upsert["required"].as_array().unwrap();
515 assert_eq!(required, &vec![json!("messageId")]);
516 let chunk = serde_json::to_value(schemars::schema_for!(SessionMessageChunk)).unwrap();
517 let required = chunk["required"].as_array().unwrap();
518 assert_eq!(required.len(), 2);
519 assert!(required.contains(&json!("messageId")));
520 assert!(required.contains(&json!("content")));
521 }
522
523 #[test]
524 fn endpoints_can_arrive_late_or_be_omitted_from_later_events() {
525 let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
526 let minimal = SessionMessage::new("m1");
527 let first = SessionMessageChunk::new("m1", block.clone());
528 assert_eq!(
529 serde_json::to_value(&minimal).unwrap(),
530 json!({"messageId": "m1"})
531 );
532 assert_eq!(
533 serde_json::to_value(&first).unwrap(),
534 json!({"messageId": "m1", "content": {"type": "text", "text": "hello"}})
535 );
536 let enriched = SessionMessage::new("m1")
537 .sender_session_id(SessionId::new("parent"))
538 .recipient_session_id(SessionId::new("child"));
539 assert_eq!(enriched.sender_session_id, Some(SessionId::new("parent")));
540 assert_eq!(enriched.recipient_session_id, Some(SessionId::new("child")));
541 let later =
542 SessionMessageChunk::new("m1", block).sender_session_id(SessionId::new("parent"));
543 assert_eq!(later.sender_session_id, Some(SessionId::new("parent")));
544 assert_eq!(later.recipient_session_id, None);
545 for update in [
546 SessionUpdate::SessionMessage(minimal),
547 SessionUpdate::SessionMessageChunk(first),
548 SessionUpdate::SessionMessage(enriched),
549 SessionUpdate::SessionMessageChunk(later),
550 ] {
551 let wire = serde_json::to_value(&update).unwrap();
552 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
553 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
554 }
555 }
556
557 #[test]
558 fn invalid_or_null_endpoints_are_absent_but_message_id_is_required() {
559 for (kind, content) in [
560 ("session_message", None),
561 (
562 "session_message_chunk",
563 Some(json!({"type": "text", "text": "hello"})),
564 ),
565 ] {
566 let mut base = json!({"sessionUpdate": kind, "messageId": "m1",
567 "senderSessionId": null, "recipientSessionId": 42});
568 if let Some(content) = content {
569 base["content"] = content;
570 }
571 let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap();
572 let encoded = serde_json::to_value(decoded).unwrap();
573 base.as_object_mut().unwrap().remove("senderSessionId");
574 base.as_object_mut().unwrap().remove("recipientSessionId");
575 assert_eq!(encoded, base);
576 for bad in [Value::Null, json!(42)] {
577 let mut invalid = base.clone();
578 invalid["messageId"] = bad;
579 assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
580 }
581 }
582 }
583}
584
585#[cfg(all(test, not(feature = "unstable_subagents")))]
586mod disabled_session_message_tests {
587 use super::*;
588 use serde_json::json;
589
590 #[test]
591 fn preserves_both_unknown_message_discriminators() {
592 for (kind, content) in [
593 ("session_message", json!([])),
594 (
595 "session_message_chunk",
596 json!({"type": "text", "text": "hello"}),
597 ),
598 ] {
599 let wire = json!({"sessionUpdate": kind, "messageId": "m1",
600 "senderSessionId": "parent", "recipientSessionId": "child", "content": content});
601 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
602 assert!(matches!(decoded, SessionUpdate::Other(_)));
603 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
604 }
605 }
606}
607
608#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
610#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
611#[serde(rename_all = "snake_case")]
612#[non_exhaustive]
613pub enum NoticeSeverity {
614 Info,
616 Warning,
618 Error,
620 #[serde(untagged)]
625 Other(String),
626}
627
628#[serde_as]
636#[skip_serializing_none]
637#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
638#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
639#[serde(rename_all = "camelCase")]
640#[non_exhaustive]
641pub struct Notice {
642 pub severity: NoticeSeverity,
644 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
646 pub title: String,
647 #[serde_as(deserialize_as = "DefaultOnError")]
651 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
652 #[serde(default)]
653 pub description: Option<String>,
654 #[serde_as(deserialize_as = "DefaultOnError")]
658 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
659 #[serde(default, rename = "_meta")]
660 pub meta: Option<Meta>,
661}
662
663impl Notice {
664 #[must_use]
666 pub fn new(severity: NoticeSeverity, title: impl Into<String>) -> Self {
667 Self {
668 severity,
669 title: title.into(),
670 description: None,
671 meta: None,
672 }
673 }
674
675 #[must_use]
677 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
678 self.description = description.into_option();
679 self
680 }
681
682 #[must_use]
684 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
685 self.meta = meta.into_option();
686 self
687 }
688}
689
690#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
693#[serde(transparent)]
694#[from(forward)]
695#[non_exhaustive]
696pub struct CompactionId(pub Arc<str>);
697
698impl CompactionId {
699 #[must_use]
701 pub fn new(id: impl Into<Self>) -> Self {
702 id.into()
703 }
704}
705
706#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
708#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
709#[serde(rename_all = "snake_case")]
710#[non_exhaustive]
711pub enum CompactionStatus {
712 #[default]
714 InProgress,
715 Completed,
717 Failed,
719 Cancelled,
721 #[serde(untagged)]
726 Other(String),
727}
728
729#[serde_as]
737#[skip_serializing_none]
738#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
739#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
740#[serde(rename_all = "camelCase")]
741#[non_exhaustive]
742pub struct CompactionUpdate {
743 pub compaction_id: CompactionId,
745 pub status: CompactionStatus,
747 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
749 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
750 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
751 pub summary: MaybeUndefined<Vec<ContentBlock>>,
752 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
754 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
755 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
756 pub error: MaybeUndefined<String>,
757 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
766 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
767 #[serde(
768 rename = "_meta",
769 default,
770 skip_serializing_if = "MaybeUndefined::is_undefined"
771 )]
772 pub meta: MaybeUndefined<Meta>,
773}
774
775impl CompactionUpdate {
776 #[must_use]
778 pub fn new(compaction_id: impl Into<CompactionId>, status: CompactionStatus) -> Self {
779 Self {
780 compaction_id: compaction_id.into(),
781 status,
782 summary: MaybeUndefined::Undefined,
783 error: MaybeUndefined::Undefined,
784 meta: MaybeUndefined::Undefined,
785 }
786 }
787
788 #[must_use]
790 pub fn summary(mut self, summary: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
791 self.summary = summary.into_maybe_undefined();
792 self
793 }
794
795 #[must_use]
797 pub fn error(mut self, error: impl IntoMaybeUndefined<String>) -> Self {
798 self.error = error.into_maybe_undefined();
799 self
800 }
801
802 #[must_use]
807 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
808 self.meta = meta.into_maybe_undefined();
809 self
810 }
811}
812
813#[serde_as]
816#[skip_serializing_none]
817#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
818#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
819#[serde(rename_all = "camelCase")]
820#[non_exhaustive]
821pub struct CompactionSummaryChunk {
822 pub compaction_id: CompactionId,
824 pub content: ContentBlock,
826 #[serde_as(deserialize_as = "DefaultOnError")]
828 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
829 #[serde(default, rename = "_meta")]
830 pub meta: Option<Meta>,
831}
832
833impl CompactionSummaryChunk {
834 #[must_use]
836 pub fn new(compaction_id: impl Into<CompactionId>, content: ContentBlock) -> Self {
837 Self {
838 compaction_id: compaction_id.into(),
839 content,
840 meta: None,
841 }
842 }
843
844 #[must_use]
846 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
847 self.meta = meta.into_option();
848 self
849 }
850}
851
852#[cfg(feature = "unstable_subagents")]
887#[serde_as]
888#[skip_serializing_none]
889#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
890#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
891#[serde(rename_all = "camelCase")]
892#[non_exhaustive]
893pub struct SubagentUpdate {
894 pub session_id: SessionId,
896 #[serde_as(deserialize_as = "DefaultOnError")]
901 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
902 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
903 pub title: MaybeUndefined<String>,
904 #[serde_as(deserialize_as = "DefaultOnError")]
909 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
910 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
911 pub description: MaybeUndefined<String>,
912 #[serde_as(deserialize_as = "DefaultOnError")]
917 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
918 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
919 pub capabilities: MaybeUndefined<SubagentSessionCapabilities>,
920 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
926 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
927 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
928 pub state: MaybeUndefined<StateUpdate>,
929 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
938 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
939 #[serde(
940 rename = "_meta",
941 default,
942 skip_serializing_if = "MaybeUndefined::is_undefined"
943 )]
944 pub meta: MaybeUndefined<Meta>,
945}
946
947#[cfg(feature = "unstable_subagents")]
948impl SubagentUpdate {
949 #[must_use]
951 pub fn new(session_id: impl Into<SessionId>) -> Self {
952 Self {
953 session_id: session_id.into(),
954 title: MaybeUndefined::Undefined,
955 description: MaybeUndefined::Undefined,
956 capabilities: MaybeUndefined::Undefined,
957 state: MaybeUndefined::Undefined,
958 meta: MaybeUndefined::Undefined,
959 }
960 }
961
962 #[must_use]
964 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
965 self.title = title.into_maybe_undefined();
966 self
967 }
968
969 #[must_use]
971 pub fn description(mut self, description: impl IntoMaybeUndefined<String>) -> Self {
972 self.description = description.into_maybe_undefined();
973 self
974 }
975
976 #[must_use]
978 pub fn capabilities(
979 mut self,
980 capabilities: impl IntoMaybeUndefined<SubagentSessionCapabilities>,
981 ) -> Self {
982 self.capabilities = capabilities.into_maybe_undefined();
983 self
984 }
985
986 #[must_use]
988 pub fn state(mut self, state: impl IntoMaybeUndefined<StateUpdate>) -> Self {
989 self.state = state.into_maybe_undefined();
990 self
991 }
992
993 #[must_use]
998 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
999 self.meta = meta.into_maybe_undefined();
1000 self
1001 }
1002}
1003
1004#[cfg(feature = "unstable_subagents")]
1013#[serde_as]
1014#[skip_serializing_none]
1015#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1016#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1017#[serde(rename_all = "camelCase")]
1018#[non_exhaustive]
1019pub struct SubagentSessionCapabilities {
1020 #[serde_as(deserialize_as = "DefaultOnError")]
1024 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1025 #[serde(default)]
1026 pub cancel: Option<SessionCancelCapabilities>,
1027 #[serde_as(deserialize_as = "DefaultOnError")]
1031 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1032 #[serde(default)]
1033 #[serde(rename = "_meta")]
1034 pub meta: Option<Meta>,
1035}
1036
1037#[cfg(feature = "unstable_subagents")]
1038impl SubagentSessionCapabilities {
1039 #[must_use]
1041 pub fn new() -> Self {
1042 Self::default()
1043 }
1044
1045 #[must_use]
1047 pub fn cancel(mut self, cancel: impl IntoOption<SessionCancelCapabilities>) -> Self {
1048 self.cancel = cancel.into_option();
1049 self
1050 }
1051
1052 #[must_use]
1056 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1057 self.meta = meta.into_option();
1058 self
1059 }
1060}
1061
1062#[cfg(feature = "unstable_subagents")]
1070#[serde_as]
1071#[skip_serializing_none]
1072#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1073#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1074#[non_exhaustive]
1075pub struct SessionCancelCapabilities {
1076 #[serde_as(deserialize_as = "DefaultOnError")]
1082 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1083 #[serde(default)]
1084 #[serde(rename = "_meta")]
1085 pub meta: Option<Meta>,
1086}
1087
1088#[cfg(feature = "unstable_subagents")]
1089impl SessionCancelCapabilities {
1090 #[must_use]
1092 pub fn new() -> Self {
1093 Self::default()
1094 }
1095
1096 #[must_use]
1102 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1103 self.meta = meta.into_option();
1104 self
1105 }
1106}
1107
1108#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1114#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1115#[cfg_attr(feature = "schemars", schemars(inline))]
1116#[cfg_attr(feature = "schemars", schemars(transform = other_session_update_schema))]
1117#[serde(rename_all = "camelCase")]
1118#[non_exhaustive]
1119pub struct OtherSessionUpdate {
1120 #[serde(rename = "sessionUpdate")]
1126 pub session_update: String,
1127 #[serde(flatten)]
1129 pub fields: BTreeMap<String, serde_json::Value>,
1130}
1131
1132impl OtherSessionUpdate {
1133 #[must_use]
1135 pub fn new(
1136 session_update: impl Into<String>,
1137 mut fields: BTreeMap<String, serde_json::Value>,
1138 ) -> Self {
1139 fields.remove("sessionUpdate");
1140 Self {
1141 session_update: session_update.into(),
1142 fields,
1143 }
1144 }
1145}
1146
1147impl<'de> Deserialize<'de> for OtherSessionUpdate {
1148 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1149 where
1150 D: serde::Deserializer<'de>,
1151 {
1152 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1153 let session_update = fields
1154 .remove("sessionUpdate")
1155 .ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
1156 let serde_json::Value::String(session_update) = session_update else {
1157 return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
1158 };
1159
1160 if is_known_session_update(&session_update) {
1161 return Err(serde::de::Error::custom(format!(
1162 "known session update `{session_update}` did not match its schema"
1163 )));
1164 }
1165
1166 Ok(Self {
1167 session_update,
1168 fields,
1169 })
1170 }
1171}
1172
1173fn is_known_session_update(session_update: &str) -> bool {
1174 if session_update == "notice" {
1175 return true;
1176 }
1177 if matches!(
1178 session_update,
1179 "compaction_update" | "compaction_summary_chunk"
1180 ) {
1181 return true;
1182 }
1183 #[cfg(feature = "unstable_plan_operations")]
1184 if session_update == "plan_removed" {
1185 return true;
1186 }
1187 #[cfg(feature = "unstable_subagents")]
1188 if matches!(
1189 session_update,
1190 "subagent_update" | "session_message" | "session_message_chunk"
1191 ) {
1192 return true;
1193 }
1194 matches!(
1195 session_update,
1196 "user_message_chunk"
1197 | "user_message"
1198 | "agent_message_chunk"
1199 | "agent_message"
1200 | "agent_thought_chunk"
1201 | "agent_thought"
1202 | "state_update"
1203 | "tool_call_content_chunk"
1204 | "tool_call_update"
1205 | "terminal_update"
1206 | "terminal_output_chunk"
1207 | "plan_update"
1208 | "available_commands_update"
1209 | "config_option_update"
1210 | "session_info_update"
1211 | "usage_update"
1212 )
1213}
1214
1215#[cfg(feature = "schemars")]
1216fn other_session_update_schema(schema: &mut Schema) {
1217 super::schema_util::reject_known_string_discriminators(
1218 schema,
1219 "sessionUpdate",
1220 &[
1221 "user_message_chunk",
1222 "user_message",
1223 "agent_message_chunk",
1224 "agent_message",
1225 "agent_thought_chunk",
1226 "agent_thought",
1227 "state_update",
1228 "tool_call_content_chunk",
1229 "tool_call_update",
1230 "terminal_update",
1231 "terminal_output_chunk",
1232 "plan_update",
1233 "available_commands_update",
1234 "config_option_update",
1235 "session_info_update",
1236 #[cfg(feature = "unstable_plan_operations")]
1237 "plan_removed",
1238 "usage_update",
1239 "notice",
1240 "compaction_update",
1241 "compaction_summary_chunk",
1242 #[cfg(feature = "unstable_subagents")]
1243 "subagent_update",
1244 #[cfg(feature = "unstable_subagents")]
1245 "session_message",
1246 #[cfg(feature = "unstable_subagents")]
1247 "session_message_chunk",
1248 ],
1249 );
1250}
1251
1252#[serde_as]
1254#[skip_serializing_none]
1255#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1257#[serde(rename_all = "camelCase")]
1258#[non_exhaustive]
1259pub struct ConfigOptionUpdate {
1260 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_>>")]
1262 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1263 pub config_options: Vec<SessionConfigOption>,
1264 #[serde_as(deserialize_as = "DefaultOnError")]
1270 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1271 #[serde(default)]
1272 #[serde(rename = "_meta")]
1273 pub meta: Option<Meta>,
1274}
1275
1276impl ConfigOptionUpdate {
1277 #[must_use]
1279 pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
1280 Self {
1281 config_options,
1282 meta: None,
1283 }
1284 }
1285
1286 #[must_use]
1292 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1293 self.meta = meta.into_option();
1294 self
1295 }
1296}
1297
1298#[serde_as]
1307#[skip_serializing_none]
1308#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1309#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1310#[serde(rename_all = "camelCase")]
1311#[non_exhaustive]
1312pub struct SessionInfoUpdate {
1313 #[serde_as(deserialize_as = "DefaultOnError")]
1315 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1316 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1317 pub title: MaybeUndefined<String>,
1318 #[serde_as(deserialize_as = "DefaultOnError")]
1320 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
1321 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1322 pub updated_at: MaybeUndefined<String>,
1323 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1332 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1333 #[serde(
1334 rename = "_meta",
1335 default,
1336 skip_serializing_if = "MaybeUndefined::is_undefined"
1337 )]
1338 pub meta: MaybeUndefined<Meta>,
1339}
1340
1341impl SessionInfoUpdate {
1342 #[must_use]
1344 pub fn new() -> Self {
1345 Self::default()
1346 }
1347
1348 #[must_use]
1350 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1351 self.title = title.into_maybe_undefined();
1352 self
1353 }
1354
1355 #[must_use]
1357 pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
1358 self.updated_at = updated_at.into_maybe_undefined();
1359 self
1360 }
1361
1362 #[must_use]
1367 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1368 self.meta = meta.into_maybe_undefined();
1369 self
1370 }
1371}
1372
1373#[serde_as]
1375#[skip_serializing_none]
1376#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1378#[serde(rename_all = "camelCase")]
1379#[non_exhaustive]
1380pub struct UsageUpdate {
1381 pub used: u64,
1383 pub size: u64,
1385 #[serde_as(deserialize_as = "DefaultOnError")]
1387 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1388 #[serde(default)]
1389 pub cost: Option<Cost>,
1390 #[serde_as(deserialize_as = "DefaultOnError")]
1396 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1397 #[serde(default)]
1398 #[serde(rename = "_meta")]
1399 pub meta: Option<Meta>,
1400}
1401
1402impl UsageUpdate {
1403 #[must_use]
1405 pub fn new(used: u64, size: u64) -> Self {
1406 Self {
1407 used,
1408 size,
1409 cost: None,
1410 meta: None,
1411 }
1412 }
1413
1414 #[must_use]
1416 pub fn cost(mut self, cost: impl IntoOption<Cost>) -> Self {
1417 self.cost = cost.into_option();
1418 self
1419 }
1420
1421 #[must_use]
1427 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1428 self.meta = meta.into_option();
1429 self
1430 }
1431}
1432
1433#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1474#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1475#[serde(tag = "state", rename_all = "snake_case")]
1476#[non_exhaustive]
1477pub enum StateUpdate {
1478 Running(RunningStateUpdate),
1480 Idle(IdleStateUpdate),
1482 RequiresAction(RequiresActionStateUpdate),
1484 #[cfg(feature = "unstable_subagents")]
1491 Unknown(UnknownStateUpdate),
1492 #[serde(untagged)]
1498 Other(OtherStateUpdate),
1499}
1500
1501#[serde_as]
1503#[skip_serializing_none]
1504#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1505#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1506#[serde(rename_all = "camelCase")]
1507#[non_exhaustive]
1508pub struct RunningStateUpdate {
1509 #[serde_as(deserialize_as = "DefaultOnError")]
1515 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1516 #[serde(default)]
1517 #[serde(rename = "_meta")]
1518 pub meta: Option<Meta>,
1519}
1520
1521impl RunningStateUpdate {
1522 #[must_use]
1524 pub fn new() -> Self {
1525 Self::default()
1526 }
1527
1528 #[must_use]
1534 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1535 self.meta = meta.into_option();
1536 self
1537 }
1538}
1539
1540#[serde_as]
1549#[skip_serializing_none]
1550#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1551#[cfg_attr(feature = "schemars", schemars(transform = idle_state_update_schema))]
1552#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1553#[serde(rename_all = "camelCase")]
1554#[non_exhaustive]
1555pub struct IdleStateUpdate {
1556 #[serde(flatten)]
1558 pub stop_reason: Option<StopReason>,
1559 #[cfg(feature = "unstable_end_turn_token_usage")]
1568 #[serde_as(deserialize_as = "DefaultOnError")]
1569 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1570 #[serde(default)]
1571 pub usage: Option<Box<Usage>>,
1572 #[serde_as(deserialize_as = "DefaultOnError")]
1578 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1579 #[serde(default)]
1580 #[serde(rename = "_meta")]
1581 pub meta: Option<Meta>,
1582}
1583
1584impl IdleStateUpdate {
1585 #[must_use]
1587 pub fn new() -> Self {
1588 Self::default()
1589 }
1590
1591 #[must_use]
1593 pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
1594 self.stop_reason = stop_reason.into_option();
1595 self
1596 }
1597
1598 #[cfg(feature = "unstable_end_turn_token_usage")]
1604 #[must_use]
1605 pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
1606 self.usage = usage.into_option().map(Box::new);
1607 self
1608 }
1609
1610 #[must_use]
1616 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1617 self.meta = meta.into_option();
1618 self
1619 }
1620}
1621
1622#[cfg(feature = "schemars")]
1631fn idle_state_update_schema(schema: &mut Schema) {
1632 if let Some(properties) = schema
1633 .get_mut("properties")
1634 .and_then(serde_json::Value::as_object_mut)
1635 {
1636 properties.insert(
1637 "stopReason".into(),
1638 serde_json::json!({
1639 "description": "Why foreground work stopped. The value selects one of this type's variants, which may add fields of their own.\n\nOptional. Omitted or `null` both mean the agent is not reporting a stop reason; a malformed value is treated the same way.\n\nSee protocol docs: [Stop Reasons](https://agentclientprotocol.com/protocol/prompt-lifecycle#stop-reasons)",
1640 "type": ["string", "null"],
1641 "x-deserialize-default-on-error": true
1642 }),
1643 );
1644 }
1645 let Some(variants) = schema
1646 .get_mut("anyOf")
1647 .and_then(serde_json::Value::as_array_mut)
1648 else {
1649 return;
1650 };
1651 for variant in variants {
1652 if variant.as_object().is_some_and(serde_json::Map::is_empty) {
1653 *variant = serde_json::json!({
1654 "title": "none",
1655 "description": "No stop reason: `stopReason` is omitted or `null`.",
1656 "type": "object",
1657 "properties": {
1658 "stopReason": { "type": "null" }
1659 }
1660 });
1661 }
1662 }
1663}
1664
1665#[serde_as]
1674#[skip_serializing_none]
1675#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1676#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1677#[serde(rename_all = "camelCase")]
1678#[non_exhaustive]
1679pub struct RequiresActionStateUpdate {
1680 #[serde_as(deserialize_as = "DefaultOnError")]
1686 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1687 #[serde(default)]
1688 #[serde(rename = "_meta")]
1689 pub meta: Option<Meta>,
1690}
1691
1692impl RequiresActionStateUpdate {
1693 #[must_use]
1695 pub fn new() -> Self {
1696 Self::default()
1697 }
1698
1699 #[must_use]
1705 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1706 self.meta = meta.into_option();
1707 self
1708 }
1709}
1710
1711#[cfg(feature = "unstable_subagents")]
1725#[serde_as]
1726#[skip_serializing_none]
1727#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1728#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1729#[serde(rename_all = "camelCase")]
1730#[non_exhaustive]
1731pub struct UnknownStateUpdate {
1732 #[serde_as(deserialize_as = "DefaultOnError")]
1736 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1737 #[serde(default, rename = "_meta")]
1738 pub meta: Option<Meta>,
1739}
1740
1741#[cfg(feature = "unstable_subagents")]
1742impl UnknownStateUpdate {
1743 #[must_use]
1745 pub fn new() -> Self {
1746 Self::default()
1747 }
1748
1749 #[must_use]
1751 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1752 self.meta = meta.into_option();
1753 self
1754 }
1755}
1756
1757#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1762#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1763#[cfg_attr(feature = "schemars", schemars(inline))]
1764#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
1765#[serde(rename_all = "camelCase")]
1766#[non_exhaustive]
1767pub struct OtherStateUpdate {
1768 #[serde(rename = "state")]
1774 pub state: String,
1775 #[serde(flatten)]
1777 pub fields: BTreeMap<String, serde_json::Value>,
1778}
1779
1780impl OtherStateUpdate {
1781 #[must_use]
1783 pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1784 fields.remove("state");
1785 Self {
1786 state: state.into(),
1787 fields,
1788 }
1789 }
1790}
1791
1792impl<'de> Deserialize<'de> for OtherStateUpdate {
1793 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1794 where
1795 D: serde::Deserializer<'de>,
1796 {
1797 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1798 let state = fields
1799 .remove("state")
1800 .ok_or_else(|| serde::de::Error::missing_field("state"))?;
1801 let serde_json::Value::String(state) = state else {
1802 return Err(serde::de::Error::custom("`state` must be a string"));
1803 };
1804
1805 if is_known_state_update(&state) {
1806 return Err(serde::de::Error::custom(format!(
1807 "known state update `{state}` did not match its schema"
1808 )));
1809 }
1810
1811 Ok(Self { state, fields })
1812 }
1813}
1814
1815const KNOWN_STATE_UPDATE_STATES: &[&str] = &[
1816 "running",
1817 "idle",
1818 "requires_action",
1819 #[cfg(feature = "unstable_subagents")]
1820 "unknown",
1821];
1822
1823fn is_known_state_update(state: &str) -> bool {
1824 KNOWN_STATE_UPDATE_STATES.contains(&state)
1825}
1826
1827#[cfg(feature = "schemars")]
1828fn other_state_update_schema(schema: &mut Schema) {
1829 super::schema_util::reject_known_string_discriminators(
1830 schema,
1831 "state",
1832 KNOWN_STATE_UPDATE_STATES,
1833 );
1834}
1835
1836#[serde_as]
1838#[skip_serializing_none]
1839#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1840#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1841#[serde(rename_all = "camelCase")]
1842#[non_exhaustive]
1843pub struct Cost {
1844 pub amount: f64,
1846 #[cfg_attr(feature = "schemars", schemars(pattern(r"^[A-Z]{3}$")))]
1848 pub currency: String,
1849 #[serde_as(deserialize_as = "DefaultOnError")]
1855 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1856 #[serde(default)]
1857 #[serde(rename = "_meta")]
1858 pub meta: Option<Meta>,
1859}
1860
1861impl Cost {
1862 #[must_use]
1864 pub fn new(amount: f64, currency: impl Into<String>) -> Self {
1865 Self {
1866 amount,
1867 currency: currency.into(),
1868 meta: None,
1869 }
1870 }
1871
1872 #[must_use]
1878 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1879 self.meta = meta.into_option();
1880 self
1881 }
1882}
1883
1884#[serde_as]
1886#[skip_serializing_none]
1887#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1888#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1889#[serde(rename_all = "camelCase")]
1890#[non_exhaustive]
1891pub struct ContentChunk {
1892 pub message_id: MessageId,
1897 pub content: ContentBlock,
1899 #[serde_as(deserialize_as = "DefaultOnError")]
1905 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1906 #[serde(default)]
1907 #[serde(rename = "_meta")]
1908 pub meta: Option<Meta>,
1909}
1910
1911impl ContentChunk {
1912 #[must_use]
1914 pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
1915 Self {
1916 content,
1917 message_id: message_id.into(),
1918 meta: None,
1919 }
1920 }
1921
1922 #[must_use]
1928 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1929 self.meta = meta.into_option();
1930 self
1931 }
1932}
1933
1934#[serde_as]
1948#[skip_serializing_none]
1949#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1950#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1951#[serde(rename_all = "camelCase")]
1952#[non_exhaustive]
1953pub struct UserMessage {
1954 pub message_id: MessageId,
1956 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
1958 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1959 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1960 pub content: MaybeUndefined<Vec<ContentBlock>>,
1961 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1970 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1971 #[serde(
1972 rename = "_meta",
1973 default,
1974 skip_serializing_if = "MaybeUndefined::is_undefined"
1975 )]
1976 pub meta: MaybeUndefined<Meta>,
1977}
1978
1979impl UserMessage {
1980 #[must_use]
1982 pub fn new(message_id: impl Into<MessageId>) -> Self {
1983 Self {
1984 message_id: message_id.into(),
1985 content: MaybeUndefined::Undefined,
1986 meta: MaybeUndefined::Undefined,
1987 }
1988 }
1989
1990 #[must_use]
1992 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
1993 self.content = content.into_maybe_undefined();
1994 self
1995 }
1996
1997 #[must_use]
2002 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2003 self.meta = meta.into_maybe_undefined();
2004 self
2005 }
2006}
2007
2008#[serde_as]
2022#[skip_serializing_none]
2023#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2024#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2025#[serde(rename_all = "camelCase")]
2026#[non_exhaustive]
2027pub struct AgentMessage {
2028 pub message_id: MessageId,
2030 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2032 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2033 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2034 pub content: MaybeUndefined<Vec<ContentBlock>>,
2035 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2044 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2045 #[serde(
2046 rename = "_meta",
2047 default,
2048 skip_serializing_if = "MaybeUndefined::is_undefined"
2049 )]
2050 pub meta: MaybeUndefined<Meta>,
2051}
2052
2053impl AgentMessage {
2054 #[must_use]
2056 pub fn new(message_id: impl Into<MessageId>) -> Self {
2057 Self {
2058 message_id: message_id.into(),
2059 content: MaybeUndefined::Undefined,
2060 meta: MaybeUndefined::Undefined,
2061 }
2062 }
2063
2064 #[must_use]
2066 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2067 self.content = content.into_maybe_undefined();
2068 self
2069 }
2070
2071 #[must_use]
2076 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2077 self.meta = meta.into_maybe_undefined();
2078 self
2079 }
2080}
2081
2082#[serde_as]
2096#[skip_serializing_none]
2097#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2098#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2099#[serde(rename_all = "camelCase")]
2100#[non_exhaustive]
2101pub struct AgentThought {
2102 pub message_id: MessageId,
2104 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2106 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2107 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2108 pub content: MaybeUndefined<Vec<ContentBlock>>,
2109 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2118 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2119 #[serde(
2120 rename = "_meta",
2121 default,
2122 skip_serializing_if = "MaybeUndefined::is_undefined"
2123 )]
2124 pub meta: MaybeUndefined<Meta>,
2125}
2126
2127impl AgentThought {
2128 #[must_use]
2130 pub fn new(message_id: impl Into<MessageId>) -> Self {
2131 Self {
2132 message_id: message_id.into(),
2133 content: MaybeUndefined::Undefined,
2134 meta: MaybeUndefined::Undefined,
2135 }
2136 }
2137
2138 #[must_use]
2140 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2141 self.content = content.into_maybe_undefined();
2142 self
2143 }
2144
2145 #[must_use]
2150 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2151 self.meta = meta.into_maybe_undefined();
2152 self
2153 }
2154}
2155
2156#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
2163#[serde(transparent)]
2164#[from(forward)]
2165#[non_exhaustive]
2166pub struct MessageId(pub Arc<str>);
2167
2168impl MessageId {
2169 #[must_use]
2171 pub fn new(id: impl Into<Self>) -> Self {
2172 id.into()
2173 }
2174}
2175
2176#[serde_as]
2178#[skip_serializing_none]
2179#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2181#[serde(rename_all = "camelCase")]
2182#[non_exhaustive]
2183pub struct AvailableCommandsUpdate {
2184 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_>>")]
2186 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2187 pub available_commands: Vec<AvailableCommand>,
2188 #[serde_as(deserialize_as = "DefaultOnError")]
2194 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2195 #[serde(default)]
2196 #[serde(rename = "_meta")]
2197 pub meta: Option<Meta>,
2198}
2199
2200impl AvailableCommandsUpdate {
2201 #[must_use]
2203 pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
2204 Self {
2205 available_commands,
2206 meta: None,
2207 }
2208 }
2209
2210 #[must_use]
2216 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2217 self.meta = meta.into_option();
2218 self
2219 }
2220}
2221
2222#[serde_as]
2224#[skip_serializing_none]
2225#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2227#[serde(rename_all = "camelCase")]
2228#[non_exhaustive]
2229pub struct AvailableCommand {
2230 pub name: String,
2232 pub description: String,
2234 #[serde_as(deserialize_as = "DefaultOnError")]
2236 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2237 #[serde(default)]
2238 pub input: Option<AvailableCommandInput>,
2239 #[serde_as(deserialize_as = "DefaultOnError")]
2245 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2246 #[serde(default)]
2247 #[serde(rename = "_meta")]
2248 pub meta: Option<Meta>,
2249}
2250
2251impl AvailableCommand {
2252 #[must_use]
2254 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
2255 Self {
2256 name: name.into(),
2257 description: description.into(),
2258 input: None,
2259 meta: None,
2260 }
2261 }
2262
2263 #[must_use]
2265 pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
2266 self.input = input.into_option();
2267 self
2268 }
2269
2270 #[must_use]
2276 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2277 self.meta = meta.into_option();
2278 self
2279 }
2280}
2281
2282#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2285#[serde(tag = "type", rename_all = "snake_case")]
2286#[non_exhaustive]
2287pub enum AvailableCommandInput {
2288 #[serde(rename = "text")]
2290 Text(TextCommandInput),
2291 #[serde(untagged)]
2302 Other(OtherAvailableCommandInput),
2303}
2304
2305#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2307#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2308#[cfg_attr(feature = "schemars", schemars(inline))]
2309#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
2310#[serde(rename_all = "camelCase")]
2311#[non_exhaustive]
2312pub struct OtherAvailableCommandInput {
2313 #[serde(rename = "type")]
2319 pub type_: String,
2320 #[serde(flatten)]
2322 pub fields: BTreeMap<String, serde_json::Value>,
2323}
2324
2325impl OtherAvailableCommandInput {
2326 #[must_use]
2328 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2329 fields.remove("type");
2330 Self {
2331 type_: type_.into(),
2332 fields,
2333 }
2334 }
2335}
2336
2337impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
2338 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2339 where
2340 D: serde::Deserializer<'de>,
2341 {
2342 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2343 let type_ = fields
2344 .remove("type")
2345 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2346 let serde_json::Value::String(type_) = type_ else {
2347 return Err(serde::de::Error::custom("`type` must be a string"));
2348 };
2349
2350 if is_known_available_command_input_type(&type_) {
2351 return Err(serde::de::Error::custom(format!(
2352 "known available command input type `{type_}` did not match its schema"
2353 )));
2354 }
2355
2356 Ok(Self { type_, fields })
2357 }
2358}
2359
2360const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
2361
2362fn is_known_available_command_input_type(type_: &str) -> bool {
2363 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
2364}
2365
2366#[cfg(feature = "schemars")]
2367fn other_available_command_input_schema(schema: &mut Schema) {
2368 super::schema_util::reject_known_string_discriminators(
2369 schema,
2370 "type",
2371 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
2372 );
2373}
2374
2375#[serde_as]
2377#[skip_serializing_none]
2378#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2380#[serde(rename_all = "camelCase")]
2381#[non_exhaustive]
2382pub struct TextCommandInput {
2383 pub hint: String,
2385 #[serde_as(deserialize_as = "DefaultOnError")]
2391 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2392 #[serde(default)]
2393 #[serde(rename = "_meta")]
2394 pub meta: Option<Meta>,
2395}
2396
2397impl TextCommandInput {
2398 #[must_use]
2400 pub fn new(hint: impl Into<String>) -> Self {
2401 Self {
2402 hint: hint.into(),
2403 meta: None,
2404 }
2405 }
2406
2407 #[must_use]
2413 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2414 self.meta = meta.into_option();
2415 self
2416 }
2417}
2418
2419#[serde_as]
2427#[skip_serializing_none]
2428#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2430#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
2431#[serde(rename_all = "camelCase")]
2432#[non_exhaustive]
2433pub struct RequestPermissionRequest {
2434 pub session_id: SessionId,
2436 pub title: String,
2441 #[serde_as(deserialize_as = "DefaultOnError")]
2447 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2448 #[serde(default)]
2449 pub description: Option<String>,
2450 #[serde(default)]
2454 pub subject: Option<RequestPermissionSubject>,
2455 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
2458 pub options: Vec<PermissionOption>,
2459 #[serde_as(deserialize_as = "DefaultOnError")]
2465 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2466 #[serde(default)]
2467 #[serde(rename = "_meta")]
2468 pub meta: Option<Meta>,
2469}
2470
2471impl RequestPermissionRequest {
2472 #[must_use]
2474 pub fn new(
2475 session_id: impl Into<SessionId>,
2476 title: impl Into<String>,
2477 options: Vec<PermissionOption>,
2478 ) -> Self {
2479 Self {
2480 session_id: session_id.into(),
2481 title: title.into(),
2482 description: None,
2483 subject: None,
2484 options,
2485 meta: None,
2486 }
2487 }
2488
2489 #[must_use]
2491 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
2492 self.description = description.into_option();
2493 self
2494 }
2495
2496 #[must_use]
2498 pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
2499 self.subject = subject.into_option();
2500 self
2501 }
2502
2503 #[must_use]
2509 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2510 self.meta = meta.into_option();
2511 self
2512 }
2513}
2514
2515#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2518#[serde(tag = "type", rename_all = "snake_case")]
2519#[non_exhaustive]
2520pub enum RequestPermissionSubject {
2521 ToolCall(Box<ToolCallPermissionSubject>),
2523 Command(CommandPermissionSubject),
2525 #[serde(untagged)]
2536 Other(OtherRequestPermissionSubject),
2537}
2538
2539impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
2540 fn from(subject: ToolCallPermissionSubject) -> Self {
2541 Self::ToolCall(Box::new(subject))
2542 }
2543}
2544
2545impl From<ToolCallUpdate> for RequestPermissionSubject {
2546 fn from(tool_call: ToolCallUpdate) -> Self {
2547 ToolCallPermissionSubject::new(tool_call).into()
2548 }
2549}
2550
2551impl From<CommandPermissionSubject> for RequestPermissionSubject {
2552 fn from(subject: CommandPermissionSubject) -> Self {
2553 Self::Command(subject)
2554 }
2555}
2556
2557#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2559#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2560#[serde(rename_all = "camelCase")]
2561#[non_exhaustive]
2562pub struct ToolCallPermissionSubject {
2563 pub tool_call: ToolCallUpdate,
2565}
2566
2567impl ToolCallPermissionSubject {
2568 #[must_use]
2570 pub fn new(tool_call: ToolCallUpdate) -> Self {
2571 Self { tool_call }
2572 }
2573}
2574
2575#[serde_as]
2577#[skip_serializing_none]
2578#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2579#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2580#[serde(rename_all = "camelCase")]
2581#[non_exhaustive]
2582pub struct CommandPermissionSubject {
2583 pub command: String,
2585 pub cwd: AbsolutePath,
2587 #[serde_as(deserialize_as = "DefaultOnError")]
2589 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2590 #[serde(default)]
2591 pub tool_call_id: Option<ToolCallId>,
2592 #[serde_as(deserialize_as = "DefaultOnError")]
2594 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2595 #[serde(default)]
2596 pub terminal_id: Option<TerminalId>,
2597 #[serde_as(deserialize_as = "DefaultOnError")]
2603 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2604 #[serde(default)]
2605 #[serde(rename = "_meta")]
2606 pub meta: Option<Meta>,
2607}
2608
2609impl CommandPermissionSubject {
2610 #[must_use]
2612 pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
2613 Self {
2614 command: command.into(),
2615 cwd: cwd.into(),
2616 tool_call_id: None,
2617 terminal_id: None,
2618 meta: None,
2619 }
2620 }
2621
2622 #[must_use]
2624 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
2625 self.tool_call_id = tool_call_id.into_option();
2626 self
2627 }
2628
2629 #[must_use]
2631 pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
2632 self.terminal_id = terminal_id.into_option();
2633 self
2634 }
2635
2636 #[must_use]
2638 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2639 self.meta = meta.into_option();
2640 self
2641 }
2642}
2643
2644#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2646#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2647#[cfg_attr(feature = "schemars", schemars(inline))]
2648#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
2649#[serde(rename_all = "camelCase")]
2650#[non_exhaustive]
2651pub struct OtherRequestPermissionSubject {
2652 #[serde(rename = "type")]
2658 pub type_: String,
2659 #[serde(flatten)]
2661 pub fields: BTreeMap<String, serde_json::Value>,
2662}
2663
2664impl OtherRequestPermissionSubject {
2665 #[must_use]
2667 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2668 fields.remove("type");
2669 Self {
2670 type_: type_.into(),
2671 fields,
2672 }
2673 }
2674}
2675
2676impl<'de> Deserialize<'de> for OtherRequestPermissionSubject {
2677 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2678 where
2679 D: serde::Deserializer<'de>,
2680 {
2681 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2682 let type_ = fields
2683 .remove("type")
2684 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2685 let serde_json::Value::String(type_) = type_ else {
2686 return Err(serde::de::Error::custom("`type` must be a string"));
2687 };
2688
2689 if is_known_request_permission_subject_type(&type_) {
2690 return Err(serde::de::Error::custom(format!(
2691 "known request permission subject `{type_}` did not match its schema"
2692 )));
2693 }
2694
2695 Ok(Self { type_, fields })
2696 }
2697}
2698
2699fn is_known_request_permission_subject_type(type_: &str) -> bool {
2700 matches!(type_, "tool_call" | "command")
2701}
2702
2703#[cfg(feature = "schemars")]
2704fn other_request_permission_subject_schema(schema: &mut Schema) {
2705 super::schema_util::reject_known_string_discriminators(
2706 schema,
2707 "type",
2708 &["tool_call", "command"],
2709 );
2710}
2711
2712#[serde_as]
2714#[skip_serializing_none]
2715#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2716#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2717#[serde(rename_all = "camelCase")]
2718#[non_exhaustive]
2719pub struct PermissionOption {
2720 pub option_id: PermissionOptionId,
2722 pub name: String,
2724 pub kind: PermissionOptionKind,
2726 #[serde_as(deserialize_as = "DefaultOnError")]
2732 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2733 #[serde(default)]
2734 #[serde(rename = "_meta")]
2735 pub meta: Option<Meta>,
2736}
2737
2738impl PermissionOption {
2739 #[must_use]
2741 pub fn new(
2742 option_id: impl Into<PermissionOptionId>,
2743 name: impl Into<String>,
2744 kind: PermissionOptionKind,
2745 ) -> Self {
2746 Self {
2747 option_id: option_id.into(),
2748 name: name.into(),
2749 kind,
2750 meta: None,
2751 }
2752 }
2753
2754 #[must_use]
2760 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2761 self.meta = meta.into_option();
2762 self
2763 }
2764}
2765
2766#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2768#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
2769#[serde(transparent)]
2770#[from(forward)]
2771#[non_exhaustive]
2772pub struct PermissionOptionId(pub Arc<str>);
2773
2774impl PermissionOptionId {
2775 #[must_use]
2777 pub fn new(id: impl Into<Self>) -> Self {
2778 id.into()
2779 }
2780}
2781
2782#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2786#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2787#[serde(rename_all = "snake_case")]
2788#[non_exhaustive]
2789pub enum PermissionOptionKind {
2790 AllowOnce,
2792 AllowAlways,
2794 RejectOnce,
2796 RejectAlways,
2798 #[serde(untagged)]
2804 Other(String),
2805}
2806
2807#[serde_as]
2809#[skip_serializing_none]
2810#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2811#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2812#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
2813#[serde(rename_all = "camelCase")]
2814#[non_exhaustive]
2815pub struct RequestPermissionResponse {
2816 pub outcome: RequestPermissionOutcome,
2818 #[serde_as(deserialize_as = "DefaultOnError")]
2824 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2825 #[serde(default)]
2826 #[serde(rename = "_meta")]
2827 pub meta: Option<Meta>,
2828}
2829
2830impl RequestPermissionResponse {
2831 #[must_use]
2833 pub fn new(outcome: RequestPermissionOutcome) -> Self {
2834 Self {
2835 outcome,
2836 meta: None,
2837 }
2838 }
2839
2840 #[must_use]
2846 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2847 self.meta = meta.into_option();
2848 self
2849 }
2850}
2851
2852#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2854#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2855#[serde(tag = "outcome", rename_all = "snake_case")]
2856#[non_exhaustive]
2857pub enum RequestPermissionOutcome {
2858 Cancelled,
2867 #[serde(rename_all = "camelCase")]
2869 Selected(SelectedPermissionOutcome),
2870 #[serde(untagged)]
2881 Other(OtherRequestPermissionOutcome),
2882}
2883
2884#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2890#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2891#[cfg_attr(feature = "schemars", schemars(inline))]
2892#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
2893#[serde(rename_all = "camelCase")]
2894#[non_exhaustive]
2895pub struct OtherRequestPermissionOutcome {
2896 pub outcome: String,
2902 #[serde(flatten)]
2904 pub fields: BTreeMap<String, serde_json::Value>,
2905}
2906
2907impl OtherRequestPermissionOutcome {
2908 #[must_use]
2910 pub fn new(
2911 outcome: impl Into<String>,
2912 mut fields: BTreeMap<String, serde_json::Value>,
2913 ) -> Self {
2914 fields.remove("outcome");
2915 Self {
2916 outcome: outcome.into(),
2917 fields,
2918 }
2919 }
2920}
2921
2922impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
2923 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2924 where
2925 D: serde::Deserializer<'de>,
2926 {
2927 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2928 let outcome = fields
2929 .remove("outcome")
2930 .ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
2931 let serde_json::Value::String(outcome) = outcome else {
2932 return Err(serde::de::Error::custom("`outcome` must be a string"));
2933 };
2934
2935 if is_known_request_permission_outcome(&outcome) {
2936 return Err(serde::de::Error::custom(format!(
2937 "known request permission outcome `{outcome}` did not match its schema"
2938 )));
2939 }
2940
2941 Ok(Self { outcome, fields })
2942 }
2943}
2944
2945fn is_known_request_permission_outcome(outcome: &str) -> bool {
2946 matches!(outcome, "cancelled" | "selected")
2947}
2948
2949#[cfg(feature = "schemars")]
2950fn other_request_permission_outcome_schema(schema: &mut Schema) {
2951 super::schema_util::reject_known_string_discriminators(
2952 schema,
2953 "outcome",
2954 &["cancelled", "selected"],
2955 );
2956}
2957
2958#[serde_as]
2960#[skip_serializing_none]
2961#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2962#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2963#[serde(rename_all = "camelCase")]
2964#[non_exhaustive]
2965pub struct SelectedPermissionOutcome {
2966 pub option_id: PermissionOptionId,
2968 #[serde_as(deserialize_as = "DefaultOnError")]
2974 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2975 #[serde(default)]
2976 #[serde(rename = "_meta")]
2977 pub meta: Option<Meta>,
2978}
2979
2980impl SelectedPermissionOutcome {
2981 #[must_use]
2983 pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
2984 Self {
2985 option_id: option_id.into(),
2986 meta: None,
2987 }
2988 }
2989
2990 #[must_use]
2996 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2997 self.meta = meta.into_option();
2998 self
2999 }
3000}
3001
3002#[serde_as]
3011#[skip_serializing_none]
3012#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3013#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3014#[serde(rename_all = "camelCase")]
3015#[non_exhaustive]
3016pub struct ClientCapabilities {
3017 #[serde_as(deserialize_as = "DefaultOnError")]
3024 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3025 #[serde(default)]
3026 pub auth: Option<AuthCapabilities>,
3027 #[serde_as(deserialize_as = "DefaultOnError")]
3033 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3034 #[serde(default)]
3035 pub elicitation: Option<ElicitationCapabilities>,
3036
3037 #[serde_as(deserialize_as = "DefaultOnError")]
3043 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3044 #[serde(default)]
3045 #[serde(rename = "_meta")]
3046 pub meta: Option<Meta>,
3047}
3048
3049impl ClientCapabilities {
3050 #[must_use]
3052 pub fn new() -> Self {
3053 Self::default()
3054 }
3055
3056 #[must_use]
3060 pub fn auth(mut self, auth: impl IntoOption<AuthCapabilities>) -> Self {
3061 self.auth = auth.into_option();
3062 self
3063 }
3064
3065 #[must_use]
3068 pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
3069 self.elicitation = elicitation.into_option();
3070 self
3071 }
3072
3073 #[must_use]
3079 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3080 self.meta = meta.into_option();
3081 self
3082 }
3083}
3084
3085#[serde_as]
3091#[skip_serializing_none]
3092#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3093#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3094#[serde(rename_all = "camelCase")]
3095#[non_exhaustive]
3096pub struct AuthCapabilities {
3097 #[serde_as(deserialize_as = "DefaultOnError")]
3104 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3105 #[serde(default)]
3106 pub terminal: Option<TerminalAuthCapabilities>,
3107 #[serde_as(deserialize_as = "DefaultOnError")]
3113 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3114 #[serde(default)]
3115 #[serde(rename = "_meta")]
3116 pub meta: Option<Meta>,
3117}
3118
3119impl AuthCapabilities {
3120 #[must_use]
3122 pub fn new() -> Self {
3123 Self::default()
3124 }
3125
3126 #[must_use]
3134 pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
3135 self.terminal = terminal.into_option();
3136 self
3137 }
3138
3139 #[must_use]
3145 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3146 self.meta = meta.into_option();
3147 self
3148 }
3149}
3150
3151#[serde_as]
3157#[skip_serializing_none]
3158#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3159#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3160#[non_exhaustive]
3161pub struct TerminalAuthCapabilities {
3162 #[serde_as(deserialize_as = "DefaultOnError")]
3168 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3169 #[serde(default)]
3170 #[serde(rename = "_meta")]
3171 pub meta: Option<Meta>,
3172}
3173
3174impl TerminalAuthCapabilities {
3175 #[must_use]
3177 pub fn new() -> Self {
3178 Self::default()
3179 }
3180
3181 #[must_use]
3187 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3188 self.meta = meta.into_option();
3189 self
3190 }
3191}
3192
3193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3199#[non_exhaustive]
3200pub struct ClientMethodNames {
3201 pub session_request_permission: &'static str,
3203 pub session_update: &'static str,
3205 #[cfg(feature = "unstable_mcp_over_acp")]
3207 pub mcp_message: &'static str,
3208 pub elicitation_create: &'static str,
3210 pub elicitation_complete: &'static str,
3212}
3213
3214pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
3216 session_update: SESSION_UPDATE_NOTIFICATION,
3217 session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
3218 #[cfg(feature = "unstable_mcp_over_acp")]
3219 mcp_message: MCP_MESSAGE_METHOD_NAME,
3220 elicitation_create: ELICITATION_CREATE_METHOD_NAME,
3221 elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
3222};
3223
3224pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
3226pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
3228pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
3230pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
3232
3233#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3240#[derive(Clone, Debug, Serialize, Deserialize)]
3241#[serde(untagged)]
3242#[cfg_attr(feature = "schemars", schemars(inline))]
3243#[non_exhaustive]
3244pub enum AgentRequest {
3245 RequestPermissionRequest(Box<RequestPermissionRequest>),
3256 CreateElicitationRequest(Box<CreateElicitationRequest>),
3260 #[cfg(feature = "unstable_mcp_over_acp")]
3266 MessageMcpRequest(Box<MessageMcpRequest>),
3267 ExtMethodRequest(Box<ExtRequest>),
3275}
3276
3277impl AgentRequest {
3278 #[must_use]
3280 pub fn method(&self) -> &str {
3281 match self {
3282 Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
3283 Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
3284 #[cfg(feature = "unstable_mcp_over_acp")]
3285 Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
3286 Self::ExtMethodRequest(ext_request) => &ext_request.method,
3287 }
3288 }
3289}
3290
3291#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3298#[derive(Clone, Debug, Serialize, Deserialize)]
3299#[serde(untagged)]
3300#[cfg_attr(feature = "schemars", schemars(inline))]
3301#[non_exhaustive]
3302pub enum ClientResponse {
3303 RequestPermissionResponse(Box<RequestPermissionResponse>),
3305 CreateElicitationResponse(Box<CreateElicitationResponse>),
3307 #[cfg(feature = "unstable_mcp_over_acp")]
3309 MessageMcpResponse(Box<MessageMcpResponse>),
3310 ExtMethodResponse(Box<ExtResponse>),
3312}
3313
3314#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3321#[derive(Clone, Debug, Serialize, Deserialize)]
3322#[serde(untagged)]
3323#[cfg_attr(feature = "schemars", schemars(inline))]
3324#[non_exhaustive]
3325pub enum AgentNotification {
3326 UpdateSessionNotification(Box<UpdateSessionNotification>),
3339 CompleteElicitationNotification(Box<CompleteElicitationNotification>),
3343 ExtNotification(Box<ExtNotification>),
3351}
3352
3353impl AgentNotification {
3354 #[must_use]
3356 pub fn method(&self) -> &str {
3357 match self {
3358 Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
3359 Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
3360 Self::ExtNotification(ext_notification) => &ext_notification.method,
3361 }
3362 }
3363}
3364
3365#[cfg(test)]
3366mod tests {
3367 use super::*;
3368
3369 #[cfg(feature = "unstable_subagents")]
3370 #[test]
3371 fn subagent_update_serializes_as_upsert() {
3372 use serde_json::json;
3373
3374 let announced =
3375 SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
3376 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
3377 ));
3378 let wire = json!({
3379 "sessionUpdate": "subagent_update",
3380 "sessionId": "sess_child_1",
3381 "capabilities": { "cancel": {} }
3382 });
3383 assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
3384 assert_eq!(
3385 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
3386 announced
3387 );
3388
3389 let minimal = SubagentUpdate::new("sess_child_1");
3390 assert_eq!(
3391 serde_json::to_value(&minimal).unwrap(),
3392 json!({
3393 "sessionId": "sess_child_1"
3394 })
3395 );
3396 assert!(minimal.capabilities.is_undefined());
3397 assert!(minimal.state.is_undefined());
3398 assert!(minimal.meta.is_undefined());
3399
3400 let cleared_wire = json!({
3402 "sessionId": "sess_child_1",
3403 "capabilities": null,
3404 "state": null,
3405 "_meta": null
3406 });
3407 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3408 assert!(cleared.capabilities.is_null());
3409 assert!(cleared.state.is_null());
3410 assert!(cleared.meta.is_null());
3411 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3412
3413 let revoked: SubagentUpdate = serde_json::from_value(json!({
3414 "sessionId": "sess_child_1",
3415 "capabilities": {}
3416 }))
3417 .unwrap();
3418 assert_eq!(
3419 revoked.capabilities,
3420 MaybeUndefined::Value(SubagentSessionCapabilities::new())
3421 );
3422 assert!(
3423 matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
3424 );
3425
3426 assert!(
3427 serde_json::from_value::<SessionUpdate>(json!({
3428 "sessionUpdate": "subagent_update"
3429 }))
3430 .is_err()
3431 );
3432 }
3433
3434 #[cfg(feature = "unstable_subagents")]
3435 #[test]
3436 fn subagent_cancel_capability_is_optional_object() {
3437 use serde_json::json;
3438
3439 for wire in [
3440 json!({}),
3441 json!({"cancel": null}),
3442 json!({"cancel": true}),
3443 json!({"cancel": false}),
3444 ] {
3445 let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
3446 assert!(child.cancel.is_none());
3447 assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
3448 }
3449 let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
3450 assert_eq!(
3451 serde_json::to_value(&enabled).unwrap(),
3452 json!({"cancel": {}})
3453 );
3454 assert_eq!(
3455 serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
3456 enabled
3457 );
3458 let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
3459 let with_meta =
3460 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
3461 let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
3462 assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
3463 assert_eq!(
3464 serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
3465 with_meta
3466 );
3467 assert!(enabled.cancel.is_some());
3468 assert!(enabled.cancel(None).cancel.is_none());
3469 let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
3470 assert_eq!(
3471 serde_json::to_value(removed).unwrap(),
3472 json!({"sessionId": "child", "capabilities": {}})
3473 );
3474 }
3475
3476 #[cfg(feature = "unstable_subagents")]
3477 #[test]
3478 fn subagent_display_metadata_uses_patch_semantics() {
3479 use serde_json::json;
3480
3481 let update = SubagentUpdate::new("child")
3482 .title("Test investigator".to_string())
3483 .description("Investigates platform-specific test failures.".to_string());
3484 let wire = json!({
3485 "sessionId": "child",
3486 "title": "Test investigator",
3487 "description": "Investigates platform-specific test failures."
3488 });
3489 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3490 assert_eq!(
3491 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
3492 update
3493 );
3494
3495 let minimal = SubagentUpdate::new("child");
3496 assert!(minimal.title.is_undefined());
3497 assert!(minimal.description.is_undefined());
3498 assert_eq!(
3499 serde_json::to_value(&minimal).unwrap(),
3500 json!({"sessionId": "child"})
3501 );
3502 let cleared_wire = json!({
3503 "sessionId": "child", "title": null, "description": null
3504 });
3505 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3506 assert!(cleared.title.is_null());
3507 assert!(cleared.description.is_null());
3508 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3509
3510 let title_only: SubagentUpdate = serde_json::from_value(json!({
3511 "sessionId": "child", "title": "Updated title"
3512 }))
3513 .unwrap();
3514 assert_eq!(
3515 title_only.title,
3516 MaybeUndefined::Value("Updated title".to_string())
3517 );
3518 assert!(title_only.description.is_undefined());
3519 let malformed: SubagentUpdate = serde_json::from_value(json!({
3520 "sessionId": "child", "title": false, "description": false
3521 }))
3522 .unwrap();
3523 assert_eq!(malformed, minimal);
3524 }
3525
3526 #[cfg(feature = "unstable_subagents")]
3527 #[test]
3528 fn subagent_notification_keeps_parent_and_child_ids_nested() {
3529 use serde_json::json;
3530
3531 let notification = UpdateSessionNotification::new(
3532 "parent",
3533 SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
3534 );
3535 let wire = json!({
3536 "sessionId": "parent",
3537 "update": {
3538 "sessionUpdate": "subagent_update",
3539 "sessionId": "child"
3540 }
3541 });
3542 assert_eq!(serde_json::to_value(¬ification).unwrap(), wire);
3543 assert_eq!(
3544 serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
3545 notification
3546 );
3547 for malformed in [
3548 json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
3549 json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
3550 ] {
3551 assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
3552 }
3553 }
3554
3555 #[cfg(feature = "unstable_subagents")]
3556 #[test]
3557 fn subagent_mirrors_normal_child_state_notifications() {
3558 use serde_json::json;
3559
3560 let association = SubagentUpdate::new("sess_child");
3561 for state in [
3562 StateUpdate::Running(RunningStateUpdate::new()),
3563 StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
3564 StateUpdate::Unknown(UnknownStateUpdate::new()),
3565 StateUpdate::Running(RunningStateUpdate::new()),
3566 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
3567 StateUpdate::Running(RunningStateUpdate::new()),
3568 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
3569 ] {
3570 let child_notification = UpdateSessionNotification::new(
3571 association.session_id.clone(),
3572 SessionUpdate::StateUpdate(state.clone()),
3573 );
3574 let parent_notification = UpdateSessionNotification::new(
3575 "sess_parent",
3576 SessionUpdate::SubagentUpdate(association.clone().state(state)),
3577 );
3578 let child_wire = serde_json::to_value(&child_notification).unwrap();
3579 let parent_wire = serde_json::to_value(&parent_notification).unwrap();
3580 assert_eq!(child_wire["sessionId"], json!("sess_child"));
3581 assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
3582 assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
3583 assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
3584 let mut child_snapshot = child_wire["update"].clone();
3585 child_snapshot
3586 .as_object_mut()
3587 .unwrap()
3588 .remove("sessionUpdate");
3589 assert_eq!(parent_wire["update"]["state"], child_snapshot);
3590 assert_eq!(
3591 serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
3592 child_notification
3593 );
3594 assert_eq!(
3595 serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
3596 parent_notification
3597 );
3598 }
3599 }
3600
3601 #[cfg(feature = "unstable_subagents")]
3602 #[test]
3603 fn subagent_state_uses_whole_snapshot_patch_semantics() {
3604 use serde_json::json;
3605
3606 let minimal = SubagentUpdate::new("child");
3607 assert!(minimal.state.is_undefined());
3608 assert_eq!(
3609 serde_json::to_value(&minimal).unwrap(),
3610 json!({"sessionId": "child"})
3611 );
3612
3613 let cleared = minimal.clone().state(None::<StateUpdate>);
3614 assert!(cleared.state.is_null());
3615 assert_eq!(
3616 serde_json::to_value(&cleared).unwrap(),
3617 json!({"sessionId": "child", "state": null})
3618 );
3619 assert_eq!(
3620 serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
3621 .unwrap(),
3622 cleared
3623 );
3624
3625 for snapshot in [
3626 json!({"state": "running", "_meta": {"source": "worker"}}),
3627 json!({"state": "idle", "stopReason": "end_turn"}),
3628 json!({"state": "requires_action"}),
3629 json!({"state": "unknown"}),
3630 json!({"state": "_custom", "detail": {"phase": 2}}),
3631 ] {
3632 let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
3633 let update = minimal.clone().state(state.clone());
3634 let wire = json!({"sessionId": "child", "state": snapshot});
3635 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3636 assert_eq!(
3637 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
3638 update
3639 );
3640 assert_eq!(update.state, MaybeUndefined::Value(state));
3641 }
3642
3643 let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
3646 IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
3647 ));
3648 let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
3649 assert_eq!(
3650 serde_json::to_value(running).unwrap(),
3651 json!({"sessionId": "child", "state": {"state": "running"}})
3652 );
3653 for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
3654 let parsed: SubagentUpdate =
3655 serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
3656 assert_eq!(parsed, minimal);
3657 }
3658 }
3659
3660 #[cfg(feature = "unstable_subagents")]
3661 #[test]
3662 fn unknown_activity_is_a_known_state_update() {
3663 use serde_json::json;
3664
3665 let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
3666 UnknownStateUpdate::new().meta(
3667 [("source".into(), json!("worker"))]
3668 .into_iter()
3669 .collect::<Meta>(),
3670 ),
3671 ));
3672 let wire = json!({
3673 "sessionUpdate": "state_update",
3674 "state": "unknown",
3675 "_meta": { "source": "worker" }
3676 });
3677 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3678 assert_eq!(
3679 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
3680 update
3681 );
3682
3683 for meta in [json!(null), json!(false)] {
3684 let state: StateUpdate = serde_json::from_value(json!({
3685 "state": "unknown",
3686 "_meta": meta
3687 }))
3688 .unwrap();
3689 assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
3690 assert_eq!(
3691 serde_json::to_value(state).unwrap(),
3692 json!({"state": "unknown"})
3693 );
3694 }
3695 assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
3696 }
3697
3698 #[cfg(not(feature = "unstable_subagents"))]
3699 #[test]
3700 fn unknown_activity_is_preserved_without_subagents_feature() {
3701 let wire = serde_json::json!({
3702 "sessionUpdate": "state_update",
3703 "state": "unknown",
3704 "_meta": { "source": "worker" }
3705 });
3706 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
3707 let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
3708 panic!("expected unrecognized state payload");
3709 };
3710 assert_eq!(state.state, "unknown");
3711 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
3712 }
3713
3714 #[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
3715 #[test]
3716 fn subagent_schema_carries_optional_nullable_state_snapshot() {
3717 use serde_json::json;
3718
3719 let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
3720 let properties = schema["properties"].as_object().unwrap();
3721 assert!(properties.contains_key("sessionId"));
3722 assert!(properties.contains_key("title"));
3723 assert!(properties.contains_key("description"));
3724 assert!(properties.contains_key("capabilities"));
3725 assert!(properties.contains_key("state"));
3726 assert!(properties.contains_key("_meta"));
3727 assert!(!properties.contains_key("name"));
3728 assert!(!properties.contains_key("task"));
3729 assert_eq!(schema["required"], json!(["sessionId"]));
3730 assert_eq!(
3731 properties["state"]["x-deserialize-default-on-error"],
3732 json!(true)
3733 );
3734 let state_variants = properties["state"]["anyOf"].as_array().unwrap();
3735 assert!(state_variants.contains(&json!({"type": "null"})));
3736 assert!(
3737 state_variants
3738 .iter()
3739 .any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
3740 );
3741 let child =
3742 serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
3743 let variants = child["properties"]["cancel"]["anyOf"]
3744 .as_array()
3745 .expect("cancel must allow the capability object or null");
3746 assert!(variants.contains(&json!({"type": "null"})));
3747 assert!(
3748 variants
3749 .iter()
3750 .any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
3751 );
3752 assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
3753 let cancel =
3754 serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
3755 assert_eq!(cancel["type"], "object");
3756 assert!(
3757 !child["required"]
3758 .as_array()
3759 .is_some_and(|required| required.contains(&json!("cancel")))
3760 );
3761 }
3762
3763 #[cfg(not(feature = "unstable_subagents"))]
3764 #[test]
3765 fn unsupported_subagent_update_is_preserved() {
3766 let wire = serde_json::json!({
3767 "sessionUpdate": "subagent_update",
3768 "sessionId": "sess_child",
3769 "capabilities": { "cancel": {} }
3770 });
3771 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
3772 assert!(matches!(&parsed, SessionUpdate::Other(_)));
3773 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
3774 }
3775
3776 #[test]
3777 fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
3778 use serde_json::json;
3779
3780 let mut meta = Meta::new();
3781 meta.insert("source".into(), json!("fallback"));
3782 let v2_notice = SessionUpdate::Notice(
3783 Notice::new(NoticeSeverity::Error, "Provider degraded")
3784 .description("Requests may take longer than usual.")
3785 .meta(meta.clone()),
3786 );
3787 let expected = json!({
3788 "sessionUpdate": "notice",
3789 "severity": "error",
3790 "title": "Provider degraded",
3791 "description": "Requests may take longer than usual.",
3792 "_meta": { "source": "fallback" }
3793 });
3794 assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
3795
3796 let v1_notice = crate::v1::SessionUpdate::Notice(
3797 crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
3798 .description("Requests may take longer than usual.")
3799 .meta(meta),
3800 );
3801 assert_eq!(
3802 serde_json::to_value(v2_notice).unwrap(),
3803 serde_json::to_value(v1_notice).unwrap()
3804 );
3805
3806 let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
3807 "sessionUpdate": "notice",
3808 "severity": "critical",
3809 "title": "Provider degraded",
3810 "description": null,
3811 "_meta": null
3812 }))
3813 .unwrap() else {
3814 panic!("expected notice");
3815 };
3816
3817 assert_eq!(
3818 notice.severity,
3819 NoticeSeverity::Other("critical".to_string())
3820 );
3821 assert_eq!(notice.description, None);
3822 assert_eq!(notice.meta, None);
3823 assert_eq!(
3824 serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
3825 json!({
3826 "sessionUpdate": "notice",
3827 "severity": "critical",
3828 "title": "Provider degraded"
3829 })
3830 );
3831 }
3832
3833 #[test]
3834 fn malformed_known_notice_is_not_hidden_as_unknown() {
3835 use serde_json::json;
3836
3837 for malformed in [
3838 json!({
3839 "sessionUpdate": "notice",
3840 "severity": "warning",
3841 "title": 42
3842 }),
3843 json!({
3844 "sessionUpdate": "notice",
3845 "severity": 42,
3846 "title": "MCP server unavailable"
3847 }),
3848 json!({
3849 "sessionUpdate": "notice",
3850 "severity": "warning"
3851 }),
3852 json!({
3853 "sessionUpdate": "notice",
3854 "severity": "warning",
3855 "title": null
3856 }),
3857 json!({
3858 "sessionUpdate": "notice",
3859 "title": "MCP server unavailable"
3860 }),
3861 json!({
3862 "sessionUpdate": "notice",
3863 "severity": null,
3864 "title": "MCP server unavailable"
3865 }),
3866 ] {
3867 assert!(serde_json::from_str::<SessionUpdate>(&malformed.to_string()).is_err());
3868 assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
3869 }
3870 }
3871
3872 #[test]
3873 fn notice_tolerates_invalid_optional_display_fields_and_custom_severity() {
3874 use serde_json::json;
3875
3876 let wire = json!({
3877 "sessionUpdate": "notice",
3878 "severity": "_custom",
3879 "title": "Provider degraded",
3880 "description": 42,
3881 "_meta": false
3882 });
3883 let SessionUpdate::Notice(notice) = serde_json::from_value(wire.clone()).unwrap() else {
3884 panic!("expected notice");
3885 };
3886 assert_eq!(
3887 serde_json::from_str::<SessionUpdate>(&wire.to_string()).unwrap(),
3888 SessionUpdate::Notice(notice.clone())
3889 );
3890 assert_eq!(notice.severity, NoticeSeverity::Other("_custom".into()));
3891 assert_eq!(notice.description, None);
3892 assert_eq!(notice.meta, None);
3893 assert_eq!(
3894 serde_json::to_value(notice).unwrap(),
3895 json!({"severity": "_custom", "title": "Provider degraded"})
3896 );
3897 }
3898
3899 #[cfg(feature = "schemars")]
3900 #[test]
3901 fn notice_schema_preserves_required_fields_and_excludes_unknown_fallback() {
3902 use serde_json::json;
3903
3904 let schema = serde_json::to_value(schemars::schema_for!(Notice)).unwrap();
3905 assert_eq!(schema["required"], json!(["severity", "title"]));
3906 assert_eq!(schema["properties"]["title"]["minLength"], 1);
3907 for field in ["description", "_meta"] {
3908 assert!(
3909 schema["properties"][field]["type"]
3910 .as_array()
3911 .unwrap()
3912 .contains(&json!("null"))
3913 );
3914 }
3915 let fallback = serde_json::to_value(schemars::schema_for!(OtherSessionUpdate)).unwrap();
3916 assert!(
3917 fallback["not"]["anyOf"]
3918 .as_array()
3919 .unwrap()
3920 .iter()
3921 .any(|variant| variant["properties"]["sessionUpdate"]["const"] == "notice")
3922 );
3923 }
3924
3925 #[test]
3926 fn compaction_updates_preserve_patch_and_open_status_semantics() {
3927 use serde_json::json;
3928
3929 assert_eq!(
3930 serde_json::to_value(SessionUpdate::CompactionUpdate(
3931 CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
3932 ContentBlock::Text(crate::v2::TextContent::new("retained")),
3933 ]),
3934 ))
3935 .unwrap(),
3936 json!({
3937 "sessionUpdate": "compaction_update",
3938 "compactionId": "cmp_001",
3939 "status": "completed",
3940 "summary": [{ "type": "text", "text": "retained" }]
3941 })
3942 );
3943
3944 let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
3945 "sessionUpdate": "compaction_update",
3946 "compactionId": "cmp_001",
3947 "status": "paused",
3948 "summary": null
3949 }))
3950 .unwrap() else {
3951 panic!("expected compaction update");
3952 };
3953 assert_eq!(update.status, CompactionStatus::Other("paused".into()));
3954 assert!(update.summary.is_null());
3955 assert!(update.error.is_undefined());
3956 }
3957
3958 #[test]
3959 fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
3960 use serde_json::json;
3961
3962 assert!(
3963 serde_json::from_value::<SessionUpdate>(json!({
3964 "sessionUpdate": "compaction_update",
3965 "status": "completed"
3966 }))
3967 .is_err()
3968 );
3969 assert!(
3970 serde_json::from_value::<SessionUpdate>(json!({
3971 "sessionUpdate": "compaction_summary_chunk",
3972 "compactionId": "cmp_001"
3973 }))
3974 .is_err()
3975 );
3976 }
3977
3978 #[test]
3979 fn test_elicitation_capability_semantics() {
3980 use serde_json::json;
3981
3982 let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
3983 assert!(unsupported.elicitation.is_none());
3984
3985 let null: ClientCapabilities =
3986 serde_json::from_value(json!({ "elicitation": null })).unwrap();
3987 assert!(null.elicitation.is_none());
3988
3989 let malformed: ClientCapabilities =
3990 serde_json::from_value(json!({ "elicitation": false })).unwrap();
3991 assert!(malformed.elicitation.is_none());
3992
3993 let empty: ClientCapabilities =
3994 serde_json::from_value(json!({ "elicitation": {} })).unwrap();
3995 let empty = empty.elicitation.expect("present capability");
3996 assert!(!empty.supports_form());
3997 assert!(!empty.supports_url());
3998
3999 let form_only: ClientCapabilities = serde_json::from_value(json!({
4000 "elicitation": { "form": {} }
4001 }))
4002 .unwrap();
4003 let form_only = form_only.elicitation.expect("advertised capability");
4004 assert!(form_only.supports_form());
4005 assert!(!form_only.supports_url());
4006
4007 let url_only: ClientCapabilities = serde_json::from_value(json!({
4008 "elicitation": { "url": {} }
4009 }))
4010 .unwrap();
4011 let url_only = url_only.elicitation.expect("advertised capability");
4012 assert!(!url_only.supports_form());
4013 assert!(url_only.supports_url());
4014
4015 let both: ClientCapabilities = serde_json::from_value(json!({
4016 "elicitation": { "form": {}, "url": {} }
4017 }))
4018 .unwrap();
4019 let both = both.elicitation.expect("advertised capability");
4020 assert!(both.supports_form());
4021 assert!(both.supports_url());
4022 }
4023
4024 #[test]
4025 fn test_elicitation_method_routing_and_envelopes() {
4026 use serde_json::json;
4027
4028 assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
4029 assert_eq!(
4030 CLIENT_METHOD_NAMES.elicitation_complete,
4031 "elicitation/complete"
4032 );
4033
4034 let request =
4035 AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
4036 crate::v2::ElicitationFormMode::new(
4037 crate::v2::ElicitationSessionScope::new("sess_1"),
4038 crate::v2::ElicitationSchema::new(),
4039 ),
4040 "Choose a value",
4041 )));
4042 assert_eq!(request.method(), "elicitation/create");
4043 let method = Arc::from(request.method());
4044 let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
4045 id: crate::v2::RequestId::Number(7),
4046 method,
4047 params: Some(request),
4048 });
4049 assert_eq!(
4050 serde_json::to_value(request).unwrap(),
4051 json!({
4052 "jsonrpc": "2.0",
4053 "id": 7,
4054 "method": "elicitation/create",
4055 "params": {
4056 "mode": "form",
4057 "sessionId": "sess_1",
4058 "message": "Choose a value",
4059 "requestedSchema": { "type": "object", "properties": {} }
4060 }
4061 })
4062 );
4063
4064 let notification = AgentNotification::CompleteElicitationNotification(Box::new(
4065 CompleteElicitationNotification::new("elic_1"),
4066 ));
4067 assert_eq!(notification.method(), "elicitation/complete");
4068 let method = Arc::from(notification.method());
4069 let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
4070 method,
4071 params: Some(notification),
4072 });
4073 assert_eq!(
4074 serde_json::to_value(notification).unwrap(),
4075 json!({
4076 "jsonrpc": "2.0",
4077 "method": "elicitation/complete",
4078 "params": { "elicitationId": "elic_1" }
4079 })
4080 );
4081 }
4082
4083 #[test]
4084 fn test_client_capabilities_auth_defaults_on_malformed_value() {
4085 use serde_json::json;
4086
4087 let capabilities: ClientCapabilities = serde_json::from_value(json!({
4088 "auth": false
4089 }))
4090 .unwrap();
4091
4092 assert_eq!(capabilities.auth, None);
4093 }
4094
4095 #[test]
4096 fn test_serialization_behavior() {
4097 use serde_json::json;
4098
4099 assert_eq!(
4100 serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
4101 SessionInfoUpdate {
4102 title: MaybeUndefined::Undefined,
4103 updated_at: MaybeUndefined::Undefined,
4104 meta: MaybeUndefined::Undefined
4105 }
4106 );
4107 assert_eq!(
4108 serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
4109 .unwrap(),
4110 SessionInfoUpdate {
4111 title: MaybeUndefined::Null,
4112 updated_at: MaybeUndefined::Null,
4113 meta: MaybeUndefined::Undefined
4114 }
4115 );
4116 assert_eq!(
4117 serde_json::from_value::<SessionInfoUpdate>(
4118 json!({"title": "title", "updatedAt": "timestamp"})
4119 )
4120 .unwrap(),
4121 SessionInfoUpdate {
4122 title: MaybeUndefined::Value("title".to_string()),
4123 updated_at: MaybeUndefined::Value("timestamp".to_string()),
4124 meta: MaybeUndefined::Undefined
4125 }
4126 );
4127
4128 let clear_meta =
4129 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
4130 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4131
4132 let mut meta = Meta::new();
4133 meta.insert("source".to_string(), json!("session-info"));
4134
4135 assert_eq!(
4136 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
4137 "source": "session-info"
4138 }}))
4139 .unwrap()
4140 .meta,
4141 MaybeUndefined::Value(meta.clone())
4142 );
4143
4144 assert_eq!(
4145 serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
4146 json!({})
4147 );
4148
4149 assert_eq!(
4150 serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
4151 json!({"_meta": null})
4152 );
4153
4154 assert_eq!(
4155 serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
4156 json!({"_meta": {
4157 "source": "session-info"
4158 }})
4159 );
4160 assert_eq!(
4161 serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
4162 json!({"title": "title"})
4163 );
4164 assert_eq!(
4165 serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
4166 json!({"title": null})
4167 );
4168 assert_eq!(
4169 serde_json::to_value(
4170 SessionInfoUpdate::new()
4171 .title("title")
4172 .title(MaybeUndefined::Undefined)
4173 )
4174 .unwrap(),
4175 json!({})
4176 );
4177 }
4178
4179 #[test]
4180 fn test_content_chunk_message_id_serialization() {
4181 use serde_json::json;
4182
4183 assert_eq!(
4184 serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
4185 ContentBlock::Text(crate::v2::TextContent::new("Hello")),
4186 "msg_agent_c42b9",
4187 )))
4188 .unwrap(),
4189 json!({
4190 "sessionUpdate": "agent_message_chunk",
4191 "messageId": "msg_agent_c42b9",
4192 "content": {
4193 "type": "text",
4194 "text": "Hello"
4195 }
4196 })
4197 );
4198
4199 let err = serde_json::from_value::<ContentChunk>(json!({
4200 "content": {
4201 "type": "text",
4202 "text": "Hello"
4203 }
4204 }))
4205 .unwrap_err();
4206
4207 assert!(err.to_string().contains("messageId"), "{err}");
4208 }
4209
4210 #[test]
4211 fn test_tool_call_content_chunk_serialization() {
4212 use serde_json::json;
4213
4214 assert_eq!(
4215 serde_json::to_value(SessionUpdate::ToolCallContentChunk(
4216 ToolCallContentChunk::new(
4217 "call_001",
4218 crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
4219 )
4220 ))
4221 .unwrap(),
4222 json!({
4223 "sessionUpdate": "tool_call_content_chunk",
4224 "toolCallId": "call_001",
4225 "content": {
4226 "type": "content",
4227 "content": {
4228 "type": "text",
4229 "text": "partial output"
4230 }
4231 }
4232 })
4233 );
4234
4235 let err = serde_json::from_value::<ToolCallContentChunk>(json!({
4236 "content": {
4237 "type": "content",
4238 "content": {
4239 "type": "text",
4240 "text": "partial output"
4241 }
4242 }
4243 }))
4244 .unwrap_err();
4245
4246 assert!(err.to_string().contains("toolCallId"), "{err}");
4247 }
4248
4249 #[test]
4250 fn test_full_message_serialization() {
4251 use serde_json::json;
4252
4253 assert_eq!(
4254 serde_json::to_value(SessionUpdate::UserMessage(
4255 UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
4256 crate::v2::TextContent::new("Hello")
4257 )])
4258 ))
4259 .unwrap(),
4260 json!({
4261 "sessionUpdate": "user_message",
4262 "messageId": "msg_user_8f7a1",
4263 "content": [
4264 {
4265 "type": "text",
4266 "text": "Hello"
4267 }
4268 ]
4269 })
4270 );
4271
4272 assert_eq!(
4273 serde_json::to_value(SessionUpdate::AgentMessage(
4274 AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
4275 crate::v2::TextContent::new("Hello")
4276 )])
4277 ))
4278 .unwrap(),
4279 json!({
4280 "sessionUpdate": "agent_message",
4281 "messageId": "msg_agent_c42b9",
4282 "content": [
4283 {
4284 "type": "text",
4285 "text": "Hello"
4286 }
4287 ]
4288 })
4289 );
4290
4291 assert_eq!(
4292 serde_json::to_value(SessionUpdate::AgentThought(
4293 AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
4294 crate::v2::TextContent::new("Need to inspect the call sites first.")
4295 )])
4296 ))
4297 .unwrap(),
4298 json!({
4299 "sessionUpdate": "agent_thought",
4300 "messageId": "msg_thought_a12",
4301 "content": [
4302 {
4303 "type": "text",
4304 "text": "Need to inspect the call sites first."
4305 }
4306 ]
4307 })
4308 );
4309 }
4310
4311 #[test]
4312 fn test_message_upsert_serialization() {
4313 use serde_json::json;
4314
4315 assert_eq!(
4316 serde_json::to_value(SessionUpdate::UserMessage(
4317 UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
4318 ))
4319 .unwrap(),
4320 json!({
4321 "sessionUpdate": "user_message",
4322 "messageId": "msg_empty",
4323 "content": []
4324 })
4325 );
4326
4327 let empty = serde_json::from_value::<UserMessage>(json!({
4328 "messageId": "msg_empty",
4329 "content": []
4330 }))
4331 .unwrap();
4332 assert!(matches!(
4333 empty.content,
4334 MaybeUndefined::Value(ref content) if content.is_empty()
4335 ));
4336
4337 let patch = serde_json::from_value::<AgentMessage>(json!({
4338 "messageId": "msg_agent_c42b9"
4339 }))
4340 .unwrap();
4341 assert_eq!(patch.content, MaybeUndefined::Undefined);
4342 assert_eq!(patch.meta, MaybeUndefined::Undefined);
4343
4344 let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
4345 "messageId": "msg_agent_c42b9",
4346 "_meta": false
4347 }))
4348 .unwrap();
4349 assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
4350
4351 let patch = serde_json::from_value::<AgentThought>(json!({
4352 "messageId": "msg_thought_a12"
4353 }))
4354 .unwrap();
4355 assert_eq!(patch.content, MaybeUndefined::Undefined);
4356
4357 let clear = serde_json::from_value::<UserMessage>(json!({
4358 "messageId": "msg_user_8f7a1",
4359 "content": null
4360 }))
4361 .unwrap();
4362 assert_eq!(clear.content, MaybeUndefined::Null);
4363
4364 let clear_meta = serde_json::from_value::<UserMessage>(json!({
4365 "messageId": "msg_user_8f7a1",
4366 "_meta": null
4367 }))
4368 .unwrap();
4369 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4370
4371 let mut meta = Meta::new();
4372 meta.insert("source".to_string(), json!("replay"));
4373
4374 assert_eq!(
4375 serde_json::to_value(SessionUpdate::UserMessage(
4376 UserMessage::new("msg_user_8f7a1").meta(meta)
4377 ))
4378 .unwrap(),
4379 json!({
4380 "sessionUpdate": "user_message",
4381 "messageId": "msg_user_8f7a1",
4382 "_meta": {
4383 "source": "replay"
4384 }
4385 })
4386 );
4387
4388 assert_eq!(
4389 serde_json::to_value(SessionUpdate::UserMessage(
4390 UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
4391 ))
4392 .unwrap(),
4393 json!({
4394 "sessionUpdate": "user_message",
4395 "messageId": "msg_user_8f7a1",
4396 "_meta": null
4397 })
4398 );
4399 }
4400
4401 #[test]
4402 fn test_usage_update_serialization() {
4403 use serde_json::json;
4404
4405 assert_eq!(
4406 serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
4407 53_000, 200_000
4408 )))
4409 .unwrap(),
4410 json!({
4411 "sessionUpdate": "usage_update",
4412 "used": 53000,
4413 "size": 200_000
4414 })
4415 );
4416
4417 assert_eq!(
4418 serde_json::to_value(SessionUpdate::UsageUpdate(
4419 UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
4420 ))
4421 .unwrap(),
4422 json!({
4423 "sessionUpdate": "usage_update",
4424 "used": 53000,
4425 "size": 200_000,
4426 "cost": {
4427 "amount": 0.045,
4428 "currency": "USD"
4429 }
4430 })
4431 );
4432
4433 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
4434 "sessionUpdate": "usage_update",
4435 "used": 53000,
4436 "size": 200_000,
4437 "cost": null
4438 }))
4439 .unwrap() else {
4440 panic!("expected usage update");
4441 };
4442
4443 assert_eq!(update.cost, None);
4444 }
4445
4446 #[test]
4447 fn error_stop_reason_carries_failure_beside_discriminator() {
4448 use crate::v2::{Error, ErrorStopReason};
4449 use serde_json::json;
4450
4451 let update =
4452 SessionUpdate::StateUpdate(StateUpdate::Idle(IdleStateUpdate::new().stop_reason(
4453 StopReason::from(ErrorStopReason::new().error(Error::auth_required())),
4454 )));
4455 let wire = json!({
4456 "sessionUpdate": "state_update",
4457 "state": "idle",
4458 "stopReason": "error",
4459 "error": { "code": -32000, "message": "Authentication required" }
4460 });
4461 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4462 assert_eq!(
4463 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4464 update
4465 );
4466
4467 let with_data =
4468 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::from(
4469 ErrorStopReason::new().error(
4470 Error::new(-32099, "Provider unavailable").data(json!({ "status": 503 })),
4471 ),
4472 )));
4473 let wire = json!({
4474 "state": "idle",
4475 "stopReason": "error",
4476 "error": {
4477 "code": -32099,
4478 "message": "Provider unavailable",
4479 "data": { "status": 503 }
4480 }
4481 });
4482 assert_eq!(serde_json::to_value(&with_data).unwrap(), wire);
4483 assert_eq!(
4484 serde_json::from_value::<StateUpdate>(wire).unwrap(),
4485 with_data
4486 );
4487
4488 let without_details = StateUpdate::Idle(
4491 IdleStateUpdate::new().stop_reason(StopReason::from(ErrorStopReason::new())),
4492 );
4493 for error in [
4494 None,
4495 Some(json!(null)),
4496 Some(json!("failed")),
4497 Some(json!({ "message": "missing code" })),
4498 ] {
4499 let mut wire = json!({ "state": "idle", "stopReason": "error" });
4500 if let Some(error) = error {
4501 wire["error"] = error;
4502 }
4503 assert_eq!(
4504 serde_json::from_value::<StateUpdate>(wire).unwrap(),
4505 without_details
4506 );
4507 }
4508 assert_eq!(
4509 serde_json::to_value(&without_details).unwrap(),
4510 json!({ "state": "idle", "stopReason": "error" })
4511 );
4512
4513 let parsed: StateUpdate = serde_json::from_value(json!({
4517 "state": "idle",
4518 "stopReason": "end_turn",
4519 "error": { "code": -32603, "message": "Internal error" }
4520 }))
4521 .unwrap();
4522 assert_eq!(
4523 parsed,
4524 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn))
4525 );
4526 }
4527
4528 #[test]
4529 fn flattened_stop_reason_keeps_open_enum_tolerance() {
4530 use crate::v2::OtherStopReason;
4531 use serde_json::json;
4532
4533 let meta: Meta = [("source".into(), json!("test"))].into_iter().collect();
4534
4535 for stop_reason in [None, Some(json!(null)), Some(json!(true)), Some(json!({}))] {
4538 let mut wire = json!({ "state": "idle", "_meta": { "source": "test" } });
4539 if let Some(stop_reason) = stop_reason {
4540 wire["stopReason"] = stop_reason;
4541 }
4542 let StateUpdate::Idle(idle) = serde_json::from_value(wire).unwrap() else {
4543 panic!("expected idle state update");
4544 };
4545 assert_eq!(idle.stop_reason, None);
4546 assert_eq!(idle.meta, Some(meta.clone()));
4547 }
4548
4549 let wire = json!({
4552 "state": "idle",
4553 "stopReason": "_paused",
4554 "resumeAfter": 30,
4555 "_meta": { "source": "test" }
4556 });
4557 let parsed: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
4558 let expected = StateUpdate::Idle(
4559 IdleStateUpdate::new()
4560 .stop_reason(StopReason::Other(OtherStopReason::new(
4561 "_paused",
4562 [("resumeAfter".into(), json!(30))].into_iter().collect(),
4563 )))
4564 .meta(meta),
4565 );
4566 assert_eq!(parsed, expected);
4567 assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
4568 }
4569
4570 #[test]
4571 fn known_stop_reasons_are_never_read_as_other() {
4572 use crate::v2::{ErrorStopReason, OtherStopReason};
4573 use serde_json::json;
4574
4575 let known = [
4576 StopReason::EndTurn,
4577 StopReason::MaxTokens,
4578 StopReason::MaxTurnRequests,
4579 StopReason::Refusal,
4580 StopReason::Cancelled,
4581 StopReason::Error(ErrorStopReason::new()),
4582 ];
4583 for stop_reason in &known {
4585 match stop_reason {
4586 StopReason::EndTurn
4587 | StopReason::MaxTokens
4588 | StopReason::MaxTurnRequests
4589 | StopReason::Refusal
4590 | StopReason::Cancelled
4591 | StopReason::Error(_)
4592 | StopReason::Other(_) => {}
4593 }
4594 }
4595
4596 for stop_reason in known {
4597 let update = StateUpdate::Idle(IdleStateUpdate::new().stop_reason(stop_reason));
4598 let wire = serde_json::to_value(&update).unwrap();
4599 assert_eq!(
4600 serde_json::from_value::<StateUpdate>(wire.clone()).unwrap(),
4601 update
4602 );
4603 assert!(
4604 serde_json::from_value::<OtherStopReason>(
4605 json!({ "stopReason": wire["stopReason"] })
4606 )
4607 .is_err(),
4608 "{} must not be accepted as a custom stop reason",
4609 wire["stopReason"]
4610 );
4611 }
4612 }
4613
4614 #[test]
4615 fn unknown_stop_reason_leaves_usage_on_the_idle_update() {
4616 use serde_json::json;
4617
4618 let usage = json!({ "totalTokens": 10, "inputTokens": 6, "outputTokens": 4 });
4619 let wire = json!({ "state": "idle", "stopReason": "_paused", "usage": usage });
4620 let StateUpdate::Idle(idle) = serde_json::from_value(wire.clone()).unwrap() else {
4621 panic!("expected idle state update");
4622 };
4623 let Some(StopReason::Other(other)) = &idle.stop_reason else {
4624 panic!("expected a custom stop reason");
4625 };
4626 assert!(other.fields.is_empty());
4627
4628 #[cfg(feature = "unstable_end_turn_token_usage")]
4629 {
4630 assert!(idle.usage.is_some());
4631 assert_eq!(serde_json::to_value(&idle).unwrap()["usage"], usage);
4632 }
4633 #[cfg(not(feature = "unstable_end_turn_token_usage"))]
4636 assert!(serde_json::to_value(&idle).unwrap().get("usage").is_none());
4637 }
4638
4639 #[test]
4640 fn test_state_update_serialization() {
4641 use serde_json::json;
4642
4643 assert_eq!(
4644 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
4645 RunningStateUpdate::new()
4646 )))
4647 .unwrap(),
4648 json!({
4649 "sessionUpdate": "state_update",
4650 "state": "running"
4651 })
4652 );
4653
4654 assert_eq!(
4655 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
4656 IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
4657 )))
4658 .unwrap(),
4659 json!({
4660 "sessionUpdate": "state_update",
4661 "state": "idle",
4662 "stopReason": "end_turn"
4663 })
4664 );
4665
4666 let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
4667 "sessionUpdate": "state_update",
4668 "state": "requires_action"
4669 }))
4670 .unwrap() else {
4671 panic!("expected state update");
4672 };
4673
4674 assert!(matches!(update, StateUpdate::RequiresAction(_)));
4675
4676 let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
4677 "sessionUpdate": "state_update",
4678 "state": "idle",
4679 "stopReason": null
4680 }))
4681 .unwrap() else {
4682 panic!("expected idle state update");
4683 };
4684
4685 assert_eq!(update.stop_reason, None);
4686
4687 let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
4688 serde_json::from_value(json!({
4689 "sessionUpdate": "state_update",
4690 "state": "_paused",
4691 "label": "Paused"
4692 }))
4693 .unwrap()
4694 else {
4695 panic!("expected unknown state update");
4696 };
4697
4698 assert_eq!(update.state, "_paused");
4699 assert_eq!(update.fields["label"], json!("Paused"));
4700 }
4701
4702 #[test]
4703 fn session_update_preserves_unknown_variant() {
4704 use serde_json::json;
4705
4706 let update: SessionUpdate = serde_json::from_value(json!({
4707 "sessionUpdate": "_status_badge",
4708 "label": "Indexing",
4709 "progress": 0.5
4710 }))
4711 .unwrap();
4712
4713 let SessionUpdate::Other(unknown) = update else {
4714 panic!("expected unknown session update");
4715 };
4716
4717 assert_eq!(unknown.session_update, "_status_badge");
4718 assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
4719 assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
4720
4721 assert_eq!(
4722 serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
4723 json!({
4724 "sessionUpdate": "_status_badge",
4725 "label": "Indexing",
4726 "progress": 0.5
4727 })
4728 );
4729 }
4730
4731 #[test]
4732 fn terminal_session_updates_use_known_discriminators() {
4733 use serde_json::json;
4734
4735 assert_eq!(
4736 serde_json::to_value(SessionUpdate::TerminalUpdate(
4737 TerminalUpdate::new("term_1").command("cargo test")
4738 ))
4739 .unwrap(),
4740 json!({
4741 "sessionUpdate": "terminal_update",
4742 "terminalId": "term_1",
4743 "command": "cargo test"
4744 })
4745 );
4746 assert_eq!(
4747 serde_json::to_value(SessionUpdate::TerminalOutputChunk(
4748 TerminalOutputChunk::new("term_1", "dGVzdAo=")
4749 ))
4750 .unwrap(),
4751 json!({
4752 "sessionUpdate": "terminal_output_chunk",
4753 "terminalId": "term_1",
4754 "data": "dGVzdAo="
4755 })
4756 );
4757 }
4758
4759 #[test]
4760 fn session_update_does_not_hide_malformed_known_terminal_variants() {
4761 use serde_json::json;
4762
4763 assert!(
4764 serde_json::from_value::<SessionUpdate>(json!({
4765 "sessionUpdate": "terminal_update"
4766 }))
4767 .is_err()
4768 );
4769 assert!(
4770 serde_json::from_value::<SessionUpdate>(json!({
4771 "sessionUpdate": "terminal_output_chunk",
4772 "terminalId": "term_1"
4773 }))
4774 .is_err()
4775 );
4776 }
4777
4778 #[test]
4779 fn test_plan_update_serialization() {
4780 use serde_json::json;
4781
4782 let plan_update =
4783 SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
4784 "plan-1",
4785 vec![crate::v2::PlanEntry::new(
4786 "Step 1",
4787 crate::v2::PlanEntryPriority::High,
4788 crate::v2::PlanEntryStatus::Pending,
4789 )],
4790 )));
4791
4792 assert_eq!(
4793 serde_json::to_value(plan_update).unwrap(),
4794 json!({
4795 "sessionUpdate": "plan_update",
4796 "plan": {
4797 "type": "items",
4798 "planId": "plan-1",
4799 "entries": [
4800 {
4801 "content": "Step 1",
4802 "priority": "high",
4803 "status": "pending"
4804 }
4805 ]
4806 }
4807 })
4808 );
4809 }
4810
4811 #[cfg(feature = "unstable_plan_operations")]
4812 #[test]
4813 fn test_plan_removed_serialization() {
4814 use serde_json::json;
4815
4816 assert_eq!(
4817 serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
4818 json!({
4819 "sessionUpdate": "plan_removed",
4820 "planId": "plan-1"
4821 })
4822 );
4823 }
4824
4825 #[test]
4826 fn available_command_input_preserves_unknown_typed_variant() {
4827 use serde_json::json;
4828
4829 let input: AvailableCommandInput = serde_json::from_value(json!({
4830 "type": "_choices",
4831 "hint": "Pick one",
4832 "options": ["fast", "careful"]
4833 }))
4834 .unwrap();
4835
4836 let AvailableCommandInput::Other(unknown) = input else {
4837 panic!("expected unknown command input");
4838 };
4839
4840 assert_eq!(unknown.type_, "_choices");
4841 assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
4842 assert_eq!(
4843 unknown.fields.get("options"),
4844 Some(&json!(["fast", "careful"]))
4845 );
4846 assert_eq!(
4847 serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
4848 json!({
4849 "type": "_choices",
4850 "hint": "Pick one",
4851 "options": ["fast", "careful"]
4852 })
4853 );
4854 }
4855
4856 #[test]
4857 fn available_command_input_text_uses_type_discriminator() {
4858 use serde_json::json;
4859
4860 let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
4861
4862 let json = serde_json::to_value(&input).unwrap();
4863 assert_eq!(
4864 json,
4865 json!({
4866 "type": "text",
4867 "hint": "Describe changes"
4868 })
4869 );
4870
4871 let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
4872 assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
4873 }
4874
4875 #[test]
4876 fn request_permission_subject_tool_call_uses_type_discriminator() {
4877 use serde_json::json;
4878
4879 let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
4880
4881 let json = serde_json::to_value(&subject).unwrap();
4882 assert_eq!(
4883 json,
4884 json!({
4885 "type": "tool_call",
4886 "toolCall": {
4887 "toolCallId": "call_001"
4888 }
4889 })
4890 );
4891
4892 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
4893 assert!(matches!(
4894 roundtripped,
4895 RequestPermissionSubject::ToolCall(_)
4896 ));
4897 }
4898
4899 #[test]
4900 fn request_permission_subject_command_uses_type_discriminator() {
4901 use serde_json::json;
4902
4903 let mut meta = Meta::new();
4904 meta.insert("source".to_string(), json!("shell"));
4905 let subject = RequestPermissionSubject::from(
4906 CommandPermissionSubject::new("cargo test", "/workspace/project")
4907 .tool_call_id("call_001")
4908 .terminal_id("term_1")
4909 .meta(meta),
4910 );
4911
4912 let json = serde_json::to_value(&subject).unwrap();
4913 assert_eq!(
4914 json,
4915 json!({
4916 "type": "command",
4917 "command": "cargo test",
4918 "cwd": "/workspace/project",
4919 "toolCallId": "call_001",
4920 "terminalId": "term_1",
4921 "_meta": {
4922 "source": "shell"
4923 }
4924 })
4925 );
4926
4927 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
4928 assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
4929 }
4930
4931 #[test]
4932 fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
4933 use serde_json::json;
4934
4935 let subject: RequestPermissionSubject = serde_json::from_value(json!({
4936 "type": "command",
4937 "command": "cargo test",
4938 "cwd": "/workspace/project",
4939 "toolCallId": null,
4940 "terminalId": null,
4941 "_meta": null
4942 }))
4943 .unwrap();
4944
4945 let RequestPermissionSubject::Command(subject) = subject else {
4946 panic!("expected command permission subject");
4947 };
4948 assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
4949 assert_eq!(subject.tool_call_id, None);
4950 assert_eq!(subject.terminal_id, None);
4951 assert_eq!(subject.meta, None);
4952 assert_eq!(
4953 serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
4954 json!({
4955 "type": "command",
4956 "command": "cargo test",
4957 "cwd": "/workspace/project"
4958 })
4959 );
4960 }
4961
4962 #[test]
4963 fn request_permission_subject_preserves_unknown_variant() {
4964 use serde_json::json;
4965
4966 let subject: RequestPermissionSubject = serde_json::from_value(json!({
4967 "type": "_review",
4968 "reason": "needs-review",
4969 "retryAfterSeconds": 30
4970 }))
4971 .unwrap();
4972
4973 let RequestPermissionSubject::Other(unknown) = subject else {
4974 panic!("expected unknown permission subject");
4975 };
4976
4977 assert_eq!(unknown.type_, "_review");
4978 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
4979 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
4980 assert_eq!(
4981 serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
4982 json!({
4983 "type": "_review",
4984 "reason": "needs-review",
4985 "retryAfterSeconds": 30
4986 })
4987 );
4988 }
4989
4990 #[test]
4991 fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
4992 use serde_json::json;
4993
4994 assert!(
4995 serde_json::from_value::<RequestPermissionSubject>(json!({
4996 "type": "tool_call"
4997 }))
4998 .is_err()
4999 );
5000 assert!(
5001 serde_json::from_value::<RequestPermissionSubject>(json!({
5002 "type": 1
5003 }))
5004 .is_err()
5005 );
5006 assert!(
5007 serde_json::from_value::<RequestPermissionSubject>(json!({
5008 "type": "command",
5009 "cwd": "/workspace/project"
5010 }))
5011 .is_err()
5012 );
5013 assert!(
5014 serde_json::from_value::<RequestPermissionSubject>(json!({
5015 "type": "command",
5016 "command": "cargo test"
5017 }))
5018 .is_err()
5019 );
5020 assert!(
5021 serde_json::from_value::<RequestPermissionSubject>(json!({
5022 "type": "command",
5023 "command": "cargo test",
5024 "cwd": null
5025 }))
5026 .is_err()
5027 );
5028 }
5029
5030 #[test]
5031 fn request_permission_title_and_description_are_separate_from_tool_call_content() {
5032 use serde_json::json;
5033
5034 let request =
5035 RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
5036 .description("Allow this tool to edit src/main.rs?")
5037 .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
5038 "call_001",
5039 )));
5040
5041 assert_eq!(
5042 serde_json::to_value(request).unwrap(),
5043 json!({
5044 "sessionId": "sess_abc123def456",
5045 "title": "Approve file edit?",
5046 "description": "Allow this tool to edit src/main.rs?",
5047 "subject": {
5048 "type": "tool_call",
5049 "toolCall": {
5050 "toolCallId": "call_001"
5051 }
5052 },
5053 "options": []
5054 })
5055 );
5056 }
5057
5058 #[test]
5059 fn request_permission_requires_title_and_allows_missing_subject() {
5060 use serde_json::json;
5061
5062 let request = RequestPermissionRequest::new(
5063 "sess_abc123def456",
5064 "Approve elevated permissions?",
5065 Vec::new(),
5066 );
5067
5068 assert_eq!(
5069 serde_json::to_value(request).unwrap(),
5070 json!({
5071 "sessionId": "sess_abc123def456",
5072 "title": "Approve elevated permissions?",
5073 "options": []
5074 })
5075 );
5076
5077 let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
5078 "sessionId": "sess_abc123def456",
5079 "title": "Approve elevated permissions?",
5080 "options": []
5081 }))
5082 .unwrap();
5083 assert!(missing_subject.subject.is_none());
5084
5085 let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
5086 "sessionId": "sess_abc123def456",
5087 "title": "Approve elevated permissions?",
5088 "subject": null,
5089 "options": []
5090 }))
5091 .unwrap();
5092 assert!(null_subject.subject.is_none());
5093
5094 assert!(
5095 serde_json::from_value::<RequestPermissionRequest>(json!({
5096 "sessionId": "sess_abc123def456",
5097 "options": []
5098 }))
5099 .is_err()
5100 );
5101 }
5102
5103 #[test]
5104 fn request_permission_outcome_preserves_unknown_variant() {
5105 use serde_json::json;
5106
5107 let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
5108 "outcome": "_defer",
5109 "reason": "needs-review",
5110 "retryAfterSeconds": 30
5111 }))
5112 .unwrap();
5113
5114 let RequestPermissionOutcome::Other(unknown) = outcome else {
5115 panic!("expected unknown permission outcome");
5116 };
5117
5118 assert_eq!(unknown.outcome, "_defer");
5119 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
5120 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
5121 assert_eq!(
5122 serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
5123 json!({
5124 "outcome": "_defer",
5125 "reason": "needs-review",
5126 "retryAfterSeconds": 30
5127 })
5128 );
5129 }
5130
5131 #[test]
5132 fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
5133 use serde_json::json;
5134
5135 assert!(
5136 serde_json::from_value::<RequestPermissionOutcome>(json!({
5137 "outcome": "selected"
5138 }))
5139 .is_err()
5140 );
5141 assert!(
5142 serde_json::from_value::<RequestPermissionOutcome>(json!({
5143 "outcome": 1
5144 }))
5145 .is_err()
5146 );
5147 }
5148
5149 #[test]
5150 fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
5151 use serde_json::json;
5152
5153 assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
5154 assert!(
5155 serde_json::from_value::<AvailableCommandInput>(json!({
5156 "hint": "Pick one"
5157 }))
5158 .is_err()
5159 );
5160 assert!(
5161 serde_json::from_value::<AvailableCommandInput>(json!({
5162 "type": 1,
5163 "hint": "Pick one"
5164 }))
5165 .is_err()
5166 );
5167 assert!(
5168 serde_json::from_value::<OtherAvailableCommandInput>(json!({
5169 "type": "text",
5170 "hint": "Pick one"
5171 }))
5172 .is_err()
5173 );
5174 }
5175
5176 #[cfg(feature = "unstable_mcp_over_acp")]
5177 #[test]
5178 fn test_agent_mcp_request_method_names() {
5179 use serde_json::json;
5180
5181 let params: serde_json::Map<String, serde_json::Value> =
5182 [("cursor".to_string(), json!("abc"))].into_iter().collect();
5183
5184 assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
5185 assert_eq!(
5186 AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
5187 "server-1",
5188 "req-1",
5189 "tools/list"
5190 )))
5191 .method(),
5192 "mcp/message"
5193 );
5194 assert_eq!(
5195 serde_json::to_value(
5196 MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
5197 )
5198 .unwrap(),
5199 json!({
5200 "serverId": "server-1",
5201 "requestId": "req-1",
5202 "method": "tools/list",
5203 "params": { "cursor": "abc" }
5204 })
5205 );
5206
5207 let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
5208 "serverId": "server-1",
5209 "requestId": "req-1",
5210 "method": "tools/list",
5211 "params": null,
5212 "_meta": null
5213 }))
5214 .unwrap();
5215 assert_eq!(request_with_null_params.params, None);
5216 assert_eq!(request_with_null_params.meta, None);
5217 for key in ["serverId", "requestId", "method"] {
5218 let mut value =
5219 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5220 value.as_object_mut().unwrap().remove(key);
5221 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5222 }
5223 for key in ["serverId", "requestId", "method"] {
5224 let mut value =
5225 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5226 value[key] = serde_json::Value::Null;
5227 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5228 }
5229 }
5230
5231 #[test]
5232 fn test_auth_capabilities_serialize_terminal_support_as_object() {
5233 use serde_json::json;
5234
5235 let capabilities = AuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
5236
5237 assert_eq!(
5238 serde_json::to_value(&capabilities).unwrap(),
5239 json!({
5240 "terminal": {}
5241 })
5242 );
5243
5244 let deserialized: AuthCapabilities = serde_json::from_value(json!({
5245 "terminal": false
5246 }))
5247 .unwrap();
5248 assert!(deserialized.terminal.is_none());
5249 }
5250
5251 #[test]
5252 fn request_permission_request_rejects_malformed_options() {
5253 use serde_json::json;
5254
5255 assert!(
5256 serde_json::from_value::<RequestPermissionRequest>(json!({
5257 "sessionId": "sess-1",
5258 "title": "Run tool?",
5259 "options": "not-an-array"
5260 }))
5261 .is_err()
5262 );
5263 assert!(
5264 serde_json::from_value::<RequestPermissionRequest>(json!({
5265 "sessionId": "sess-1",
5266 "title": "Run tool?",
5267 "options": [{"optionId": "allow"}]
5268 }))
5269 .is_err()
5270 );
5271 }
5272
5273 #[cfg(feature = "unstable_plan_operations")]
5274 #[test]
5275 fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
5276 use serde_json::json;
5277
5278 assert!(
5279 serde_json::from_value::<SessionUpdate>(json!({
5280 "sessionUpdate": "plan_removed"
5281 }))
5282 .is_err()
5283 );
5284 }
5285}