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")]
202#[serde_as]
203#[skip_serializing_none]
204#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206#[serde(rename_all = "camelCase")]
207#[non_exhaustive]
208pub struct SessionMessageChunk {
209 pub message_id: MessageId,
211 pub content: ContentBlock,
213 #[serde_as(deserialize_as = "DefaultOnError")]
220 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
221 #[serde(default, rename = "_meta")]
222 pub meta: Option<Meta>,
223}
224
225#[cfg(feature = "unstable_subagents")]
226impl SessionMessageChunk {
227 #[must_use]
229 pub fn new(message_id: impl Into<MessageId>, content: ContentBlock) -> Self {
230 Self {
231 message_id: message_id.into(),
232 content,
233 meta: None,
234 }
235 }
236
237 #[must_use]
239 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
240 self.meta = meta.into_option();
241 self
242 }
243}
244
245#[cfg(feature = "unstable_subagents")]
251#[serde_as]
252#[skip_serializing_none]
253#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
255#[serde(rename_all = "camelCase")]
256#[non_exhaustive]
257pub struct SessionMessage {
258 pub message_id: MessageId,
260 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
264 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
265 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
266 pub sender_session_id: MaybeUndefined<SessionId>,
267 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
271 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
272 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
273 pub recipient_session_id: MaybeUndefined<SessionId>,
274 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
280 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
281 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
282 pub sender_message_id: MaybeUndefined<MessageId>,
283 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
289 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
290 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
291 pub recipient_message_id: MaybeUndefined<MessageId>,
292 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
295 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
296 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
297 pub content: MaybeUndefined<Vec<ContentBlock>>,
298 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
307 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
308 #[serde(
309 default,
310 rename = "_meta",
311 skip_serializing_if = "MaybeUndefined::is_undefined"
312 )]
313 pub meta: MaybeUndefined<Meta>,
314}
315
316#[cfg(feature = "unstable_subagents")]
317impl SessionMessage {
318 #[must_use]
320 pub fn new(message_id: impl Into<MessageId>) -> Self {
321 Self {
322 message_id: message_id.into(),
323 sender_session_id: MaybeUndefined::Undefined,
324 recipient_session_id: MaybeUndefined::Undefined,
325 sender_message_id: MaybeUndefined::Undefined,
326 recipient_message_id: MaybeUndefined::Undefined,
327 content: MaybeUndefined::Undefined,
328 meta: MaybeUndefined::Undefined,
329 }
330 }
331
332 #[must_use]
334 pub fn sender_session_id(
335 mut self,
336 sender_session_id: impl IntoMaybeUndefined<SessionId>,
337 ) -> Self {
338 self.sender_session_id = sender_session_id.into_maybe_undefined();
339 self
340 }
341
342 #[must_use]
344 pub fn recipient_session_id(
345 mut self,
346 recipient_session_id: impl IntoMaybeUndefined<SessionId>,
347 ) -> Self {
348 self.recipient_session_id = recipient_session_id.into_maybe_undefined();
349 self
350 }
351
352 #[must_use]
354 pub fn sender_message_id(
355 mut self,
356 sender_message_id: impl IntoMaybeUndefined<MessageId>,
357 ) -> Self {
358 self.sender_message_id = sender_message_id.into_maybe_undefined();
359 self
360 }
361
362 #[must_use]
364 pub fn recipient_message_id(
365 mut self,
366 recipient_message_id: impl IntoMaybeUndefined<MessageId>,
367 ) -> Self {
368 self.recipient_message_id = recipient_message_id.into_maybe_undefined();
369 self
370 }
371
372 #[must_use]
374 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
375 self.content = content.into_maybe_undefined();
376 self
377 }
378
379 #[must_use]
384 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
385 self.meta = meta.into_maybe_undefined();
386 self
387 }
388}
389
390#[cfg(all(test, feature = "unstable_subagents"))]
391mod session_message_tests {
392 use super::*;
393 use serde_json::{Value, json};
394
395 #[test]
396 fn transcript_envelopes_and_multimodal_content() {
397 for (transcript, sender, recipient, id) in [
398 ("parent", "parent", "child", "sent-1"),
399 ("child", "parent", "child", "received-9"),
400 ("child", "child", "parent", "sent-2"),
401 ] {
402 let wire = json!({"sessionId": transcript, "update": {
403 "sessionUpdate": "session_message", "messageId": id,
404 "senderSessionId": sender, "recipientSessionId": recipient, "content": [
405 {"type": "text", "text": "hello"},
406 {"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"}
407 ]
408 }});
409 let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
410 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
411 }
412 }
413
414 #[test]
415 fn chunks_and_upserts_share_transcript_local_identity() {
416 for (transcript, id) in [("parent", "sent-1"), ("child", "received-9")] {
417 let wire = json!({"sessionId": transcript, "update": {
418 "sessionUpdate": "session_message_chunk", "messageId": id,
419 "content": {"type": "text", "text": "hello"}
420 }});
421 let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
422 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
423 let upsert = SessionMessage::new(id)
424 .sender_session_id(SessionId::new("parent"))
425 .recipient_session_id(SessionId::new("child"))
426 .content(vec![]);
427 let chunk = SessionMessageChunk::new(
428 id,
429 ContentBlock::Text(crate::v2::TextContent::new("hello")),
430 );
431 assert_eq!(chunk.message_id, upsert.message_id);
432 assert_eq!(
433 serde_json::to_value(SessionUpdate::SessionMessageChunk(chunk)).unwrap(),
434 wire["update"]
435 );
436 assert_eq!(serde_json::to_value(upsert).unwrap()["content"], json!([]));
437 }
438 let with_identity = json!({"sessionUpdate": "session_message_chunk", "messageId": "m1",
440 "senderSessionId": "parent", "recipientSessionId": "child",
441 "senderMessageId": "sent-1", "recipientMessageId": "received-1",
442 "content": {"type": "text", "text": "hello"}});
443 let decoded: SessionUpdate = serde_json::from_value(with_identity).unwrap();
444 assert_eq!(
445 serde_json::to_value(decoded).unwrap(),
446 json!({"sessionUpdate": "session_message_chunk", "messageId": "m1",
447 "content": {"type": "text", "text": "hello"}})
448 );
449 }
450
451 #[test]
452 fn required_ids_and_chunk_content_with_upsert_patch_semantics() {
453 for (kind, content) in [
454 ("session_message", json!([])),
455 (
456 "session_message_chunk",
457 json!({"type": "text", "text": "hello"}),
458 ),
459 ] {
460 let mut base = json!({"sessionUpdate": kind, "messageId": "m1", "content": content});
461 if kind == "session_message" {
462 base["senderSessionId"] = json!("parent");
463 base["recipientSessionId"] = json!("child");
464 }
465 {
466 let key = "messageId";
467 let mut missing = base.clone();
468 missing.as_object_mut().unwrap().remove(key);
469 assert!(
470 serde_json::from_value::<SessionUpdate>(missing).is_err(),
471 "{kind} {key}"
472 );
473 let mut null = base.clone();
474 null[key] = Value::Null;
475 assert!(
476 serde_json::from_value::<SessionUpdate>(null).is_err(),
477 "{kind} {key}"
478 );
479 let mut non_string = base.clone();
480 non_string[key] = json!(42);
481 assert!(
482 serde_json::from_value::<SessionUpdate>(non_string).is_err(),
483 "{kind} {key}"
484 );
485 }
486 if kind == "session_message_chunk" {
487 for content in [None, Some(Value::Null), Some(json!([]))] {
488 let mut invalid = base.clone();
489 invalid.as_object_mut().unwrap().remove("content");
490 if let Some(content) = content {
491 invalid["content"] = content;
492 }
493 assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
494 }
495 } else {
496 for content in [None, Some(Value::Null), Some(json!([])), Some(json!({}))] {
497 let mut wire = base.clone();
498 wire.as_object_mut().unwrap().remove("content");
499 if let Some(content) = content {
500 wire["content"] = content;
501 }
502 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
503 let encoded = serde_json::to_value(decoded).unwrap();
504 if wire["content"].is_object() {
505 wire.as_object_mut().unwrap().remove("content");
506 }
507 assert_eq!(encoded, wire);
508 }
509 }
510 for meta in [None, Some(Value::Null), Some(json!({"tag": "value"}))] {
511 let mut wire = base.clone();
512 if let Some(meta) = meta {
513 wire["_meta"] = meta;
514 }
515 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
516 let encoded = serde_json::to_value(decoded).unwrap();
517 if kind == "session_message" {
518 assert_eq!(encoded, wire);
519 } else if wire["_meta"].is_null() {
520 assert_eq!(encoded, base);
521 } else {
522 assert_eq!(encoded, wire);
523 }
524 }
525 }
526 let metadata_only = json!({"sessionUpdate": "session_message",
527 "messageId": "m1", "senderSessionId": "parent",
528 "recipientSessionId": "child", "_meta": {"tag": "value"}});
529 let decoded: SessionUpdate = serde_json::from_value(metadata_only.clone()).unwrap();
530 assert_eq!(serde_json::to_value(decoded).unwrap(), metadata_only);
531 let cleared = SessionMessage::new("m1")
532 .sender_session_id(SessionId::new("parent"))
533 .recipient_session_id(SessionId::new("child"))
534 .content(None)
535 .meta(None);
536 assert_eq!(
537 serde_json::to_value(cleared).unwrap(),
538 json!({"messageId": "m1", "senderSessionId": "parent",
539 "recipientSessionId": "child", "content": null, "_meta": null})
540 );
541 }
542
543 #[cfg(feature = "schemars")]
544 #[test]
545 fn schemas_require_ids_and_chunk_content_but_not_upsert_content() {
546 let upsert = serde_json::to_value(schemars::schema_for!(SessionMessage)).unwrap();
547 let required = upsert["required"].as_array().unwrap();
548 assert_eq!(required, &vec![json!("messageId")]);
549 let chunk = serde_json::to_value(schemars::schema_for!(SessionMessageChunk)).unwrap();
550 let required = chunk["required"].as_array().unwrap();
551 assert_eq!(required.len(), 2);
552 assert!(required.contains(&json!("messageId")));
553 assert!(required.contains(&json!("content")));
554 }
555
556 #[test]
557 fn endpoints_can_arrive_late_or_be_omitted_from_later_events() {
558 let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
559 let minimal = SessionMessage::new("m1");
560 let first = SessionMessageChunk::new("m1", block.clone());
561 assert_eq!(
562 serde_json::to_value(&minimal).unwrap(),
563 json!({"messageId": "m1"})
564 );
565 assert_eq!(
566 serde_json::to_value(&first).unwrap(),
567 json!({"messageId": "m1", "content": {"type": "text", "text": "hello"}})
568 );
569 let enriched = SessionMessage::new("m1")
570 .sender_session_id(SessionId::new("parent"))
571 .recipient_session_id(SessionId::new("child"));
572 assert_eq!(
573 enriched.sender_session_id,
574 MaybeUndefined::Value(SessionId::new("parent"))
575 );
576 assert_eq!(
577 enriched.recipient_session_id,
578 MaybeUndefined::Value(SessionId::new("child"))
579 );
580 let later = SessionMessage::new("m1").sender_session_id(SessionId::new("parent"));
581 assert_eq!(
582 later.sender_session_id,
583 MaybeUndefined::Value(SessionId::new("parent"))
584 );
585 assert_eq!(later.recipient_session_id, MaybeUndefined::Undefined);
586 let more = SessionMessageChunk::new("m1", block);
587 for update in [
588 SessionUpdate::SessionMessage(minimal),
589 SessionUpdate::SessionMessageChunk(first),
590 SessionUpdate::SessionMessage(enriched),
591 SessionUpdate::SessionMessage(later),
592 SessionUpdate::SessionMessageChunk(more),
593 ] {
594 let wire = serde_json::to_value(&update).unwrap();
595 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
596 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
597 }
598 }
599
600 #[test]
601 fn null_endpoints_clear_invalid_are_absent_but_message_id_is_required() {
602 let mut base = json!({"sessionUpdate": "session_message", "messageId": "m1",
603 "senderSessionId": null, "recipientSessionId": 42});
604 let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap();
605 let encoded = serde_json::to_value(decoded).unwrap();
606 base.as_object_mut().unwrap().remove("recipientSessionId");
607 assert_eq!(encoded, base);
608 for bad in [Value::Null, json!(42)] {
609 let mut invalid = base.clone();
610 invalid["messageId"] = bad;
611 assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
612 }
613 }
614
615 #[test]
616 fn message_references_link_views_and_can_arrive_late() {
617 let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
618 let incoming = SessionMessage::new("received-1")
619 .sender_session_id(SessionId::new("parent"))
620 .recipient_session_id(SessionId::new("child"))
621 .sender_message_id(MessageId::new("sent-1"))
622 .content(vec![block]);
623 let outgoing =
625 SessionMessage::new("sent-1").recipient_message_id(MessageId::new("received-1"));
626 for (update, wire) in [
627 (
628 SessionUpdate::SessionMessage(incoming),
629 json!({"sessionUpdate": "session_message", "messageId": "received-1",
630 "senderSessionId": "parent", "recipientSessionId": "child",
631 "senderMessageId": "sent-1", "content": [{"type": "text", "text": "hello"}]}),
632 ),
633 (
634 SessionUpdate::SessionMessage(outgoing),
635 json!({"sessionUpdate": "session_message", "messageId": "sent-1",
636 "recipientMessageId": "received-1"}),
637 ),
638 ] {
639 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
640 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
641 assert_eq!(decoded, update);
642 }
643
644 let mut wire = json!({"sessionUpdate": "session_message", "messageId": "m1",
645 "senderMessageId": null, "recipientMessageId": 42});
646 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
647 wire.as_object_mut().unwrap().remove("recipientMessageId");
648 assert_eq!(
649 serde_json::to_value(decoded).unwrap(),
650 wire,
651 "null clears a reference and a malformed one is not supplied"
652 );
653 }
654}
655
656#[cfg(all(test, not(feature = "unstable_subagents")))]
657mod disabled_session_message_tests {
658 use super::*;
659 use serde_json::json;
660
661 #[test]
662 fn preserves_both_unknown_message_discriminators() {
663 for (kind, content) in [
664 ("session_message", json!([])),
665 (
666 "session_message_chunk",
667 json!({"type": "text", "text": "hello"}),
668 ),
669 ] {
670 let wire = json!({"sessionUpdate": kind, "messageId": "m1",
671 "senderSessionId": "parent", "recipientSessionId": "child", "content": content});
672 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
673 assert!(matches!(decoded, SessionUpdate::Other(_)));
674 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
675 }
676 }
677}
678
679#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
681#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
682#[serde(rename_all = "snake_case")]
683#[non_exhaustive]
684pub enum NoticeSeverity {
685 Info,
687 Warning,
689 Error,
691 #[serde(untagged)]
696 Other(String),
697}
698
699#[serde_as]
707#[skip_serializing_none]
708#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
709#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
710#[serde(rename_all = "camelCase")]
711#[non_exhaustive]
712pub struct Notice {
713 pub severity: NoticeSeverity,
715 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
717 pub title: String,
718 #[serde_as(deserialize_as = "DefaultOnError")]
722 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
723 #[serde(default)]
724 pub description: Option<String>,
725 #[serde_as(deserialize_as = "DefaultOnError")]
731 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
732 #[serde(default, rename = "_meta")]
733 pub meta: Option<Meta>,
734}
735
736impl Notice {
737 #[must_use]
739 pub fn new(severity: NoticeSeverity, title: impl Into<String>) -> Self {
740 Self {
741 severity,
742 title: title.into(),
743 description: None,
744 meta: None,
745 }
746 }
747
748 #[must_use]
750 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
751 self.description = description.into_option();
752 self
753 }
754
755 #[must_use]
757 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
758 self.meta = meta.into_option();
759 self
760 }
761}
762
763#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
765#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
766#[serde(transparent)]
767#[from(forward)]
768#[non_exhaustive]
769pub struct CompactionId(pub Arc<str>);
770
771impl CompactionId {
772 #[must_use]
774 pub fn new(id: impl Into<Self>) -> Self {
775 id.into()
776 }
777}
778
779#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
781#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
782#[serde(rename_all = "snake_case")]
783#[non_exhaustive]
784pub enum CompactionStatus {
785 #[default]
787 InProgress,
788 Completed,
790 Failed,
792 Cancelled,
794 #[serde(untagged)]
799 Other(String),
800}
801
802#[serde_as]
810#[skip_serializing_none]
811#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
812#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
813#[serde(rename_all = "camelCase")]
814#[non_exhaustive]
815pub struct CompactionUpdate {
816 pub compaction_id: CompactionId,
818 pub status: CompactionStatus,
820 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
822 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
823 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
824 pub summary: MaybeUndefined<Vec<ContentBlock>>,
825 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
827 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
828 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
829 pub error: MaybeUndefined<String>,
830 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
839 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
840 #[serde(
841 rename = "_meta",
842 default,
843 skip_serializing_if = "MaybeUndefined::is_undefined"
844 )]
845 pub meta: MaybeUndefined<Meta>,
846}
847
848impl CompactionUpdate {
849 #[must_use]
851 pub fn new(compaction_id: impl Into<CompactionId>, status: CompactionStatus) -> Self {
852 Self {
853 compaction_id: compaction_id.into(),
854 status,
855 summary: MaybeUndefined::Undefined,
856 error: MaybeUndefined::Undefined,
857 meta: MaybeUndefined::Undefined,
858 }
859 }
860
861 #[must_use]
863 pub fn summary(mut self, summary: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
864 self.summary = summary.into_maybe_undefined();
865 self
866 }
867
868 #[must_use]
870 pub fn error(mut self, error: impl IntoMaybeUndefined<String>) -> Self {
871 self.error = error.into_maybe_undefined();
872 self
873 }
874
875 #[must_use]
880 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
881 self.meta = meta.into_maybe_undefined();
882 self
883 }
884}
885
886#[serde_as]
889#[skip_serializing_none]
890#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
891#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
892#[serde(rename_all = "camelCase")]
893#[non_exhaustive]
894pub struct CompactionSummaryChunk {
895 pub compaction_id: CompactionId,
897 pub content: ContentBlock,
899 #[serde_as(deserialize_as = "DefaultOnError")]
906 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
907 #[serde(default, rename = "_meta")]
908 pub meta: Option<Meta>,
909}
910
911impl CompactionSummaryChunk {
912 #[must_use]
914 pub fn new(compaction_id: impl Into<CompactionId>, content: ContentBlock) -> Self {
915 Self {
916 compaction_id: compaction_id.into(),
917 content,
918 meta: None,
919 }
920 }
921
922 #[must_use]
924 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
925 self.meta = meta.into_option();
926 self
927 }
928}
929
930#[cfg(feature = "unstable_subagents")]
965#[serde_as]
966#[skip_serializing_none]
967#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
968#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
969#[serde(rename_all = "camelCase")]
970#[non_exhaustive]
971pub struct SubagentUpdate {
972 pub session_id: SessionId,
974 #[serde_as(deserialize_as = "DefaultOnError")]
979 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
980 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
981 pub title: MaybeUndefined<String>,
982 #[serde_as(deserialize_as = "DefaultOnError")]
987 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
988 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
989 pub description: MaybeUndefined<String>,
990 #[serde_as(deserialize_as = "DefaultOnError")]
995 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
996 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
997 pub capabilities: MaybeUndefined<SubagentSessionCapabilities>,
998 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1004 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1005 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1006 pub state: MaybeUndefined<StateUpdate>,
1007 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1016 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1017 #[serde(
1018 rename = "_meta",
1019 default,
1020 skip_serializing_if = "MaybeUndefined::is_undefined"
1021 )]
1022 pub meta: MaybeUndefined<Meta>,
1023}
1024
1025#[cfg(feature = "unstable_subagents")]
1026impl SubagentUpdate {
1027 #[must_use]
1029 pub fn new(session_id: impl Into<SessionId>) -> Self {
1030 Self {
1031 session_id: session_id.into(),
1032 title: MaybeUndefined::Undefined,
1033 description: MaybeUndefined::Undefined,
1034 capabilities: MaybeUndefined::Undefined,
1035 state: MaybeUndefined::Undefined,
1036 meta: MaybeUndefined::Undefined,
1037 }
1038 }
1039
1040 #[must_use]
1042 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1043 self.title = title.into_maybe_undefined();
1044 self
1045 }
1046
1047 #[must_use]
1049 pub fn description(mut self, description: impl IntoMaybeUndefined<String>) -> Self {
1050 self.description = description.into_maybe_undefined();
1051 self
1052 }
1053
1054 #[must_use]
1056 pub fn capabilities(
1057 mut self,
1058 capabilities: impl IntoMaybeUndefined<SubagentSessionCapabilities>,
1059 ) -> Self {
1060 self.capabilities = capabilities.into_maybe_undefined();
1061 self
1062 }
1063
1064 #[must_use]
1066 pub fn state(mut self, state: impl IntoMaybeUndefined<StateUpdate>) -> Self {
1067 self.state = state.into_maybe_undefined();
1068 self
1069 }
1070
1071 #[must_use]
1076 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1077 self.meta = meta.into_maybe_undefined();
1078 self
1079 }
1080}
1081
1082#[cfg(feature = "unstable_subagents")]
1091#[serde_as]
1092#[skip_serializing_none]
1093#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1094#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1095#[serde(rename_all = "camelCase")]
1096#[non_exhaustive]
1097pub struct SubagentSessionCapabilities {
1098 #[serde_as(deserialize_as = "DefaultOnError")]
1102 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1103 #[serde(default)]
1104 pub cancel: Option<SessionCancelCapabilities>,
1105 #[serde_as(deserialize_as = "DefaultOnError")]
1111 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1112 #[serde(default)]
1113 #[serde(rename = "_meta")]
1114 pub meta: Option<Meta>,
1115}
1116
1117#[cfg(feature = "unstable_subagents")]
1118impl SubagentSessionCapabilities {
1119 #[must_use]
1121 pub fn new() -> Self {
1122 Self::default()
1123 }
1124
1125 #[must_use]
1127 pub fn cancel(mut self, cancel: impl IntoOption<SessionCancelCapabilities>) -> Self {
1128 self.cancel = cancel.into_option();
1129 self
1130 }
1131
1132 #[must_use]
1136 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1137 self.meta = meta.into_option();
1138 self
1139 }
1140}
1141
1142#[cfg(feature = "unstable_subagents")]
1150#[serde_as]
1151#[skip_serializing_none]
1152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1153#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1154#[non_exhaustive]
1155pub struct SessionCancelCapabilities {
1156 #[serde_as(deserialize_as = "DefaultOnError")]
1162 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1163 #[serde(default)]
1164 #[serde(rename = "_meta")]
1165 pub meta: Option<Meta>,
1166}
1167
1168#[cfg(feature = "unstable_subagents")]
1169impl SessionCancelCapabilities {
1170 #[must_use]
1172 pub fn new() -> Self {
1173 Self::default()
1174 }
1175
1176 #[must_use]
1182 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1183 self.meta = meta.into_option();
1184 self
1185 }
1186}
1187
1188#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1194#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1195#[cfg_attr(feature = "schemars", schemars(inline))]
1196#[cfg_attr(feature = "schemars", schemars(transform = other_session_update_schema))]
1197#[serde(rename_all = "camelCase")]
1198#[non_exhaustive]
1199pub struct OtherSessionUpdate {
1200 #[serde(rename = "sessionUpdate")]
1206 pub session_update: String,
1207 #[serde(flatten)]
1209 pub fields: BTreeMap<String, serde_json::Value>,
1210}
1211
1212impl OtherSessionUpdate {
1213 #[must_use]
1215 pub fn new(
1216 session_update: impl Into<String>,
1217 mut fields: BTreeMap<String, serde_json::Value>,
1218 ) -> Self {
1219 fields.remove("sessionUpdate");
1220 Self {
1221 session_update: session_update.into(),
1222 fields,
1223 }
1224 }
1225}
1226
1227impl<'de> Deserialize<'de> for OtherSessionUpdate {
1228 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1229 where
1230 D: serde::Deserializer<'de>,
1231 {
1232 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1233 let session_update = fields
1234 .remove("sessionUpdate")
1235 .ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
1236 let serde_json::Value::String(session_update) = session_update else {
1237 return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
1238 };
1239
1240 if is_known_session_update(&session_update) {
1241 return Err(serde::de::Error::custom(format!(
1242 "known session update `{session_update}` did not match its schema"
1243 )));
1244 }
1245
1246 Ok(Self {
1247 session_update,
1248 fields,
1249 })
1250 }
1251}
1252
1253fn is_known_session_update(session_update: &str) -> bool {
1254 if session_update == "notice" {
1255 return true;
1256 }
1257 if matches!(
1258 session_update,
1259 "compaction_update" | "compaction_summary_chunk"
1260 ) {
1261 return true;
1262 }
1263 #[cfg(feature = "unstable_plan_operations")]
1264 if session_update == "plan_removed" {
1265 return true;
1266 }
1267 #[cfg(feature = "unstable_subagents")]
1268 if matches!(
1269 session_update,
1270 "subagent_update" | "session_message" | "session_message_chunk"
1271 ) {
1272 return true;
1273 }
1274 matches!(
1275 session_update,
1276 "user_message_chunk"
1277 | "user_message"
1278 | "agent_message_chunk"
1279 | "agent_message"
1280 | "agent_thought_chunk"
1281 | "agent_thought"
1282 | "state_update"
1283 | "tool_call_content_chunk"
1284 | "tool_call_update"
1285 | "terminal_update"
1286 | "terminal_output_chunk"
1287 | "plan_update"
1288 | "available_commands_update"
1289 | "config_option_update"
1290 | "session_info_update"
1291 | "usage_update"
1292 )
1293}
1294
1295#[cfg(feature = "schemars")]
1296fn other_session_update_schema(schema: &mut Schema) {
1297 super::schema_util::reject_known_string_discriminators(
1298 schema,
1299 "sessionUpdate",
1300 &[
1301 "user_message_chunk",
1302 "user_message",
1303 "agent_message_chunk",
1304 "agent_message",
1305 "agent_thought_chunk",
1306 "agent_thought",
1307 "state_update",
1308 "tool_call_content_chunk",
1309 "tool_call_update",
1310 "terminal_update",
1311 "terminal_output_chunk",
1312 "plan_update",
1313 "available_commands_update",
1314 "config_option_update",
1315 "session_info_update",
1316 #[cfg(feature = "unstable_plan_operations")]
1317 "plan_removed",
1318 "usage_update",
1319 "notice",
1320 "compaction_update",
1321 "compaction_summary_chunk",
1322 #[cfg(feature = "unstable_subagents")]
1323 "subagent_update",
1324 #[cfg(feature = "unstable_subagents")]
1325 "session_message",
1326 #[cfg(feature = "unstable_subagents")]
1327 "session_message_chunk",
1328 ],
1329 );
1330}
1331
1332#[serde_as]
1334#[skip_serializing_none]
1335#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1337#[serde(rename_all = "camelCase")]
1338#[non_exhaustive]
1339pub struct ConfigOptionUpdate {
1340 #[serde_as(deserialize_as = "VecSkipError<_>")]
1342 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
1343 pub config_options: Vec<SessionConfigOption>,
1344 #[serde_as(deserialize_as = "DefaultOnError")]
1350 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1351 #[serde(default)]
1352 #[serde(rename = "_meta")]
1353 pub meta: Option<Meta>,
1354}
1355
1356impl ConfigOptionUpdate {
1357 #[must_use]
1359 pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
1360 Self {
1361 config_options,
1362 meta: None,
1363 }
1364 }
1365
1366 #[must_use]
1372 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1373 self.meta = meta.into_option();
1374 self
1375 }
1376}
1377
1378#[serde_as]
1387#[skip_serializing_none]
1388#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1389#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1390#[serde(rename_all = "camelCase")]
1391#[non_exhaustive]
1392pub struct SessionInfoUpdate {
1393 #[serde_as(deserialize_as = "DefaultOnError")]
1395 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1396 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1397 pub title: MaybeUndefined<String>,
1398 #[serde_as(deserialize_as = "DefaultOnError")]
1400 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
1401 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1402 pub updated_at: MaybeUndefined<String>,
1403 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1412 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1413 #[serde(
1414 rename = "_meta",
1415 default,
1416 skip_serializing_if = "MaybeUndefined::is_undefined"
1417 )]
1418 pub meta: MaybeUndefined<Meta>,
1419}
1420
1421impl SessionInfoUpdate {
1422 #[must_use]
1424 pub fn new() -> Self {
1425 Self::default()
1426 }
1427
1428 #[must_use]
1430 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1431 self.title = title.into_maybe_undefined();
1432 self
1433 }
1434
1435 #[must_use]
1437 pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
1438 self.updated_at = updated_at.into_maybe_undefined();
1439 self
1440 }
1441
1442 #[must_use]
1447 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1448 self.meta = meta.into_maybe_undefined();
1449 self
1450 }
1451}
1452
1453#[serde_as]
1455#[skip_serializing_none]
1456#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1458#[serde(rename_all = "camelCase")]
1459#[non_exhaustive]
1460pub struct UsageUpdate {
1461 pub used: u64,
1463 pub size: u64,
1465 #[serde_as(deserialize_as = "DefaultOnError")]
1469 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1470 #[serde(default)]
1471 pub cost: Option<Cost>,
1472 #[serde_as(deserialize_as = "DefaultOnError")]
1478 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1479 #[serde(default)]
1480 #[serde(rename = "_meta")]
1481 pub meta: Option<Meta>,
1482}
1483
1484impl UsageUpdate {
1485 #[must_use]
1487 pub fn new(used: u64, size: u64) -> Self {
1488 Self {
1489 used,
1490 size,
1491 cost: None,
1492 meta: None,
1493 }
1494 }
1495
1496 #[must_use]
1498 pub fn cost(mut self, cost: impl IntoOption<Cost>) -> Self {
1499 self.cost = cost.into_option();
1500 self
1501 }
1502
1503 #[must_use]
1509 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1510 self.meta = meta.into_option();
1511 self
1512 }
1513}
1514
1515#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1557#[serde(tag = "state", rename_all = "snake_case")]
1558#[non_exhaustive]
1559pub enum StateUpdate {
1560 Running(RunningStateUpdate),
1562 Idle(IdleStateUpdate),
1564 RequiresAction(RequiresActionStateUpdate),
1566 #[cfg(feature = "unstable_subagents")]
1573 Unknown(UnknownStateUpdate),
1574 #[serde(untagged)]
1586 Other(OtherStateUpdate),
1587}
1588
1589#[serde_as]
1591#[skip_serializing_none]
1592#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1593#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1594#[serde(rename_all = "camelCase")]
1595#[non_exhaustive]
1596pub struct RunningStateUpdate {
1597 #[serde_as(deserialize_as = "DefaultOnError")]
1603 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1604 #[serde(default)]
1605 #[serde(rename = "_meta")]
1606 pub meta: Option<Meta>,
1607}
1608
1609impl RunningStateUpdate {
1610 #[must_use]
1612 pub fn new() -> Self {
1613 Self::default()
1614 }
1615
1616 #[must_use]
1622 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1623 self.meta = meta.into_option();
1624 self
1625 }
1626}
1627
1628#[serde_as]
1637#[skip_serializing_none]
1638#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1639#[cfg_attr(feature = "schemars", schemars(transform = idle_state_update_schema))]
1640#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1641#[serde(rename_all = "camelCase")]
1642#[non_exhaustive]
1643pub struct IdleStateUpdate {
1644 #[serde(flatten)]
1646 pub stop_reason: Option<StopReason>,
1647 #[cfg(feature = "unstable_end_turn_token_usage")]
1656 #[serde_as(deserialize_as = "DefaultOnError")]
1657 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1658 #[serde(default)]
1659 pub usage: Option<Box<Usage>>,
1660 #[serde_as(deserialize_as = "DefaultOnError")]
1666 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1667 #[serde(default)]
1668 #[serde(rename = "_meta")]
1669 pub meta: Option<Meta>,
1670}
1671
1672impl IdleStateUpdate {
1673 #[must_use]
1675 pub fn new() -> Self {
1676 Self::default()
1677 }
1678
1679 #[must_use]
1681 pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
1682 self.stop_reason = stop_reason.into_option();
1683 self
1684 }
1685
1686 #[cfg(feature = "unstable_end_turn_token_usage")]
1692 #[must_use]
1693 pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
1694 self.usage = usage.into_option().map(Box::new);
1695 self
1696 }
1697
1698 #[must_use]
1704 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1705 self.meta = meta.into_option();
1706 self
1707 }
1708}
1709
1710#[cfg(feature = "schemars")]
1719fn idle_state_update_schema(schema: &mut Schema) {
1720 if let Some(properties) = schema
1721 .get_mut("properties")
1722 .and_then(serde_json::Value::as_object_mut)
1723 {
1724 properties.insert(
1725 "stopReason".into(),
1726 serde_json::json!({
1727 "description": "Why foreground work stopped. The value selects one of this type's variants, which may add fields of their own.\n\nOptional. Omitted and `null` are equivalent and 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/v2/prompt-lifecycle#stop-reasons)",
1728 "type": ["string", "null"],
1729 "x-deserialize-default-on-error": true
1730 }),
1731 );
1732 }
1733 let Some(variants) = schema
1734 .get_mut("anyOf")
1735 .and_then(serde_json::Value::as_array_mut)
1736 else {
1737 return;
1738 };
1739 for variant in variants {
1740 if variant.as_object().is_some_and(serde_json::Map::is_empty) {
1741 *variant = serde_json::json!({
1742 "title": "none",
1743 "description": "No stop reason: `stopReason` is omitted or `null`.",
1744 "type": "object",
1745 "properties": {
1746 "stopReason": { "type": "null" }
1747 }
1748 });
1749 }
1750 }
1751}
1752
1753#[serde_as]
1762#[skip_serializing_none]
1763#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1764#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1765#[serde(rename_all = "camelCase")]
1766#[non_exhaustive]
1767pub struct RequiresActionStateUpdate {
1768 #[serde_as(deserialize_as = "DefaultOnError")]
1774 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1775 #[serde(default)]
1776 #[serde(rename = "_meta")]
1777 pub meta: Option<Meta>,
1778}
1779
1780impl RequiresActionStateUpdate {
1781 #[must_use]
1783 pub fn new() -> Self {
1784 Self::default()
1785 }
1786
1787 #[must_use]
1793 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1794 self.meta = meta.into_option();
1795 self
1796 }
1797}
1798
1799#[cfg(feature = "unstable_subagents")]
1813#[serde_as]
1814#[skip_serializing_none]
1815#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1816#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1817#[serde(rename_all = "camelCase")]
1818#[non_exhaustive]
1819pub struct UnknownStateUpdate {
1820 #[serde_as(deserialize_as = "DefaultOnError")]
1826 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1827 #[serde(default, rename = "_meta")]
1828 pub meta: Option<Meta>,
1829}
1830
1831#[cfg(feature = "unstable_subagents")]
1832impl UnknownStateUpdate {
1833 #[must_use]
1835 pub fn new() -> Self {
1836 Self::default()
1837 }
1838
1839 #[must_use]
1841 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1842 self.meta = meta.into_option();
1843 self
1844 }
1845}
1846
1847#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1852#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1853#[cfg_attr(feature = "schemars", schemars(inline))]
1854#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
1855#[serde(rename_all = "camelCase")]
1856#[non_exhaustive]
1857pub struct OtherStateUpdate {
1858 #[serde(rename = "state")]
1864 pub state: String,
1865 #[serde(flatten)]
1867 pub fields: BTreeMap<String, serde_json::Value>,
1868}
1869
1870impl OtherStateUpdate {
1871 #[must_use]
1873 pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1874 fields.remove("state");
1875 Self {
1876 state: state.into(),
1877 fields,
1878 }
1879 }
1880}
1881
1882impl<'de> Deserialize<'de> for OtherStateUpdate {
1883 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1884 where
1885 D: serde::Deserializer<'de>,
1886 {
1887 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1888 let state = fields
1889 .remove("state")
1890 .ok_or_else(|| serde::de::Error::missing_field("state"))?;
1891 let serde_json::Value::String(state) = state else {
1892 return Err(serde::de::Error::custom("`state` must be a string"));
1893 };
1894
1895 if is_known_state_update(&state) {
1896 return Err(serde::de::Error::custom(format!(
1897 "known state update `{state}` did not match its schema"
1898 )));
1899 }
1900
1901 Ok(Self { state, fields })
1902 }
1903}
1904
1905const KNOWN_STATE_UPDATE_STATES: &[&str] = &[
1906 "running",
1907 "idle",
1908 "requires_action",
1909 #[cfg(feature = "unstable_subagents")]
1910 "unknown",
1911];
1912
1913fn is_known_state_update(state: &str) -> bool {
1914 KNOWN_STATE_UPDATE_STATES.contains(&state)
1915}
1916
1917#[cfg(feature = "schemars")]
1918fn other_state_update_schema(schema: &mut Schema) {
1919 super::schema_util::reject_known_string_discriminators(
1920 schema,
1921 "state",
1922 KNOWN_STATE_UPDATE_STATES,
1923 );
1924}
1925
1926#[serde_as]
1928#[skip_serializing_none]
1929#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1930#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1931#[serde(rename_all = "camelCase")]
1932#[non_exhaustive]
1933pub struct Cost {
1934 pub amount: f64,
1936 #[cfg_attr(feature = "schemars", schemars(pattern(r"^[A-Z]{3}$")))]
1938 pub currency: String,
1939 #[serde_as(deserialize_as = "DefaultOnError")]
1945 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1946 #[serde(default)]
1947 #[serde(rename = "_meta")]
1948 pub meta: Option<Meta>,
1949}
1950
1951impl Cost {
1952 #[must_use]
1954 pub fn new(amount: f64, currency: impl Into<String>) -> Self {
1955 Self {
1956 amount,
1957 currency: currency.into(),
1958 meta: None,
1959 }
1960 }
1961
1962 #[must_use]
1968 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1969 self.meta = meta.into_option();
1970 self
1971 }
1972}
1973
1974#[serde_as]
1976#[skip_serializing_none]
1977#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1978#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1979#[serde(rename_all = "camelCase")]
1980#[non_exhaustive]
1981pub struct ContentChunk {
1982 pub message_id: MessageId,
1987 pub content: ContentBlock,
1989 #[serde_as(deserialize_as = "DefaultOnError")]
1996 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1997 #[serde(default)]
1998 #[serde(rename = "_meta")]
1999 pub meta: Option<Meta>,
2000}
2001
2002impl ContentChunk {
2003 #[must_use]
2005 pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
2006 Self {
2007 content,
2008 message_id: message_id.into(),
2009 meta: None,
2010 }
2011 }
2012
2013 #[must_use]
2019 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2020 self.meta = meta.into_option();
2021 self
2022 }
2023}
2024
2025#[serde_as]
2039#[skip_serializing_none]
2040#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2041#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2042#[serde(rename_all = "camelCase")]
2043#[non_exhaustive]
2044pub struct UserMessage {
2045 pub message_id: MessageId,
2047 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2049 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2050 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2051 pub content: MaybeUndefined<Vec<ContentBlock>>,
2052 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2061 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2062 #[serde(
2063 rename = "_meta",
2064 default,
2065 skip_serializing_if = "MaybeUndefined::is_undefined"
2066 )]
2067 pub meta: MaybeUndefined<Meta>,
2068}
2069
2070impl UserMessage {
2071 #[must_use]
2073 pub fn new(message_id: impl Into<MessageId>) -> Self {
2074 Self {
2075 message_id: message_id.into(),
2076 content: MaybeUndefined::Undefined,
2077 meta: MaybeUndefined::Undefined,
2078 }
2079 }
2080
2081 #[must_use]
2083 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2084 self.content = content.into_maybe_undefined();
2085 self
2086 }
2087
2088 #[must_use]
2093 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2094 self.meta = meta.into_maybe_undefined();
2095 self
2096 }
2097}
2098
2099#[serde_as]
2113#[skip_serializing_none]
2114#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2116#[serde(rename_all = "camelCase")]
2117#[non_exhaustive]
2118pub struct AgentMessage {
2119 pub message_id: MessageId,
2121 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2123 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2124 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2125 pub content: MaybeUndefined<Vec<ContentBlock>>,
2126 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2135 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2136 #[serde(
2137 rename = "_meta",
2138 default,
2139 skip_serializing_if = "MaybeUndefined::is_undefined"
2140 )]
2141 pub meta: MaybeUndefined<Meta>,
2142}
2143
2144impl AgentMessage {
2145 #[must_use]
2147 pub fn new(message_id: impl Into<MessageId>) -> Self {
2148 Self {
2149 message_id: message_id.into(),
2150 content: MaybeUndefined::Undefined,
2151 meta: MaybeUndefined::Undefined,
2152 }
2153 }
2154
2155 #[must_use]
2157 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2158 self.content = content.into_maybe_undefined();
2159 self
2160 }
2161
2162 #[must_use]
2167 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2168 self.meta = meta.into_maybe_undefined();
2169 self
2170 }
2171}
2172
2173#[serde_as]
2187#[skip_serializing_none]
2188#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2190#[serde(rename_all = "camelCase")]
2191#[non_exhaustive]
2192pub struct AgentThought {
2193 pub message_id: MessageId,
2195 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2197 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2198 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2199 pub content: MaybeUndefined<Vec<ContentBlock>>,
2200 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2209 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2210 #[serde(
2211 rename = "_meta",
2212 default,
2213 skip_serializing_if = "MaybeUndefined::is_undefined"
2214 )]
2215 pub meta: MaybeUndefined<Meta>,
2216}
2217
2218impl AgentThought {
2219 #[must_use]
2221 pub fn new(message_id: impl Into<MessageId>) -> Self {
2222 Self {
2223 message_id: message_id.into(),
2224 content: MaybeUndefined::Undefined,
2225 meta: MaybeUndefined::Undefined,
2226 }
2227 }
2228
2229 #[must_use]
2231 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2232 self.content = content.into_maybe_undefined();
2233 self
2234 }
2235
2236 #[must_use]
2241 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2242 self.meta = meta.into_maybe_undefined();
2243 self
2244 }
2245}
2246
2247#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
2254#[serde(transparent)]
2255#[from(forward)]
2256#[non_exhaustive]
2257pub struct MessageId(pub Arc<str>);
2258
2259impl MessageId {
2260 #[must_use]
2262 pub fn new(id: impl Into<Self>) -> Self {
2263 id.into()
2264 }
2265}
2266
2267#[serde_as]
2269#[skip_serializing_none]
2270#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2272#[serde(rename_all = "camelCase")]
2273#[non_exhaustive]
2274pub struct AvailableCommandsUpdate {
2275 #[serde_as(deserialize_as = "VecSkipError<_>")]
2277 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
2278 pub available_commands: Vec<AvailableCommand>,
2279 #[serde_as(deserialize_as = "DefaultOnError")]
2285 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2286 #[serde(default)]
2287 #[serde(rename = "_meta")]
2288 pub meta: Option<Meta>,
2289}
2290
2291impl AvailableCommandsUpdate {
2292 #[must_use]
2294 pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
2295 Self {
2296 available_commands,
2297 meta: None,
2298 }
2299 }
2300
2301 #[must_use]
2307 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2308 self.meta = meta.into_option();
2309 self
2310 }
2311}
2312
2313#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2318#[serde(tag = "type", rename_all = "snake_case")]
2319#[non_exhaustive]
2320pub enum AvailableCommand {
2321 Command(Command),
2323 #[serde(untagged)]
2334 Other(OtherAvailableCommand),
2335}
2336
2337impl AvailableCommand {
2338 #[must_use]
2340 pub fn name(&self) -> &str {
2341 match self {
2342 Self::Command(c) => &c.name,
2343 Self::Other(c) => &c.name,
2344 }
2345 }
2346
2347 #[must_use]
2349 pub fn description(&self) -> &str {
2350 match self {
2351 Self::Command(c) => &c.description,
2352 Self::Other(c) => &c.description,
2353 }
2354 }
2355
2356 #[must_use]
2362 pub fn meta(&self) -> Option<&Meta> {
2363 match self {
2364 Self::Command(c) => c.meta.as_ref(),
2365 Self::Other(c) => c.fields.get("_meta").and_then(serde_json::Value::as_object),
2366 }
2367 }
2368}
2369
2370#[serde_as]
2372#[skip_serializing_none]
2373#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2374#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2375#[cfg_attr(feature = "schemars", schemars(inline))]
2376#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_schema))]
2377#[serde(rename_all = "camelCase")]
2378#[non_exhaustive]
2379pub struct OtherAvailableCommand {
2380 #[serde(rename = "type")]
2386 pub type_: String,
2387 pub name: String,
2389 pub description: String,
2391 #[serde(flatten)]
2394 pub fields: BTreeMap<String, serde_json::Value>,
2395}
2396
2397impl OtherAvailableCommand {
2398 #[must_use]
2400 pub fn new(
2401 type_: impl Into<String>,
2402 name: impl Into<String>,
2403 description: impl Into<String>,
2404 mut fields: BTreeMap<String, serde_json::Value>,
2405 ) -> Self {
2406 fields.remove("type");
2407 fields.remove("name");
2408 fields.remove("description");
2409 Self {
2410 type_: type_.into(),
2411 name: name.into(),
2412 description: description.into(),
2413 fields,
2414 }
2415 }
2416}
2417
2418impl<'de> Deserialize<'de> for OtherAvailableCommand {
2419 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2420 where
2421 D: serde::Deserializer<'de>,
2422 {
2423 #[derive(Deserialize)]
2424 struct RawOtherAvailableCommand {
2425 #[serde(rename = "type")]
2426 type_: String,
2427 name: String,
2428 description: String,
2429 #[serde(flatten)]
2430 fields: BTreeMap<String, serde_json::Value>,
2431 }
2432
2433 let raw = RawOtherAvailableCommand::deserialize(deserializer)?;
2434 if is_known_available_command_type(&raw.type_) {
2435 return Err(serde::de::Error::custom(format!(
2436 "known available command type `{}` did not match its schema",
2437 raw.type_
2438 )));
2439 }
2440
2441 Ok(Self {
2442 type_: raw.type_,
2443 name: raw.name,
2444 description: raw.description,
2445 fields: raw.fields,
2446 })
2447 }
2448}
2449
2450const KNOWN_AVAILABLE_COMMAND_TYPES: &[&str] = &["command"];
2451
2452fn is_known_available_command_type(type_: &str) -> bool {
2453 KNOWN_AVAILABLE_COMMAND_TYPES.contains(&type_)
2454}
2455
2456#[cfg(feature = "schemars")]
2457fn other_available_command_schema(schema: &mut Schema) {
2458 super::schema_util::reject_known_string_discriminators(
2459 schema,
2460 "type",
2461 KNOWN_AVAILABLE_COMMAND_TYPES,
2462 );
2463}
2464
2465#[serde_as]
2469#[skip_serializing_none]
2470#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2472#[serde(rename_all = "camelCase")]
2473#[non_exhaustive]
2474pub struct Command {
2475 pub name: String,
2477 pub description: String,
2479 #[serde_as(deserialize_as = "DefaultOnError")]
2483 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2484 #[serde(default)]
2485 pub input: Option<AvailableCommandInput>,
2486 #[serde_as(deserialize_as = "DefaultOnError")]
2492 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2493 #[serde(default)]
2494 #[serde(rename = "_meta")]
2495 pub meta: Option<Meta>,
2496}
2497
2498impl Command {
2499 #[must_use]
2501 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
2502 Self {
2503 name: name.into(),
2504 description: description.into(),
2505 input: None,
2506 meta: None,
2507 }
2508 }
2509
2510 #[must_use]
2512 pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
2513 self.input = input.into_option();
2514 self
2515 }
2516
2517 #[must_use]
2523 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2524 self.meta = meta.into_option();
2525 self
2526 }
2527}
2528
2529#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2531#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2532#[serde(tag = "type", rename_all = "snake_case")]
2533#[non_exhaustive]
2534pub enum AvailableCommandInput {
2535 #[serde(rename = "text")]
2537 Text(TextCommandInput),
2538 #[serde(untagged)]
2549 Other(OtherAvailableCommandInput),
2550}
2551
2552#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2554#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2555#[cfg_attr(feature = "schemars", schemars(inline))]
2556#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
2557#[serde(rename_all = "camelCase")]
2558#[non_exhaustive]
2559pub struct OtherAvailableCommandInput {
2560 #[serde(rename = "type")]
2566 pub type_: String,
2567 #[serde(flatten)]
2569 pub fields: BTreeMap<String, serde_json::Value>,
2570}
2571
2572impl OtherAvailableCommandInput {
2573 #[must_use]
2575 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2576 fields.remove("type");
2577 Self {
2578 type_: type_.into(),
2579 fields,
2580 }
2581 }
2582}
2583
2584impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
2585 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2586 where
2587 D: serde::Deserializer<'de>,
2588 {
2589 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2590 let type_ = fields
2591 .remove("type")
2592 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2593 let serde_json::Value::String(type_) = type_ else {
2594 return Err(serde::de::Error::custom("`type` must be a string"));
2595 };
2596
2597 if is_known_available_command_input_type(&type_) {
2598 return Err(serde::de::Error::custom(format!(
2599 "known available command input type `{type_}` did not match its schema"
2600 )));
2601 }
2602
2603 Ok(Self { type_, fields })
2604 }
2605}
2606
2607const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
2608
2609fn is_known_available_command_input_type(type_: &str) -> bool {
2610 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
2611}
2612
2613#[cfg(feature = "schemars")]
2614fn other_available_command_input_schema(schema: &mut Schema) {
2615 super::schema_util::reject_known_string_discriminators(
2616 schema,
2617 "type",
2618 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
2619 );
2620}
2621
2622#[serde_as]
2624#[skip_serializing_none]
2625#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2626#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2627#[serde(rename_all = "camelCase")]
2628#[non_exhaustive]
2629pub struct TextCommandInput {
2630 pub hint: String,
2632 #[serde_as(deserialize_as = "DefaultOnError")]
2638 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2639 #[serde(default)]
2640 #[serde(rename = "_meta")]
2641 pub meta: Option<Meta>,
2642}
2643
2644impl TextCommandInput {
2645 #[must_use]
2647 pub fn new(hint: impl Into<String>) -> Self {
2648 Self {
2649 hint: hint.into(),
2650 meta: None,
2651 }
2652 }
2653
2654 #[must_use]
2660 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2661 self.meta = meta.into_option();
2662 self
2663 }
2664}
2665
2666#[serde_as]
2674#[skip_serializing_none]
2675#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2677#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
2678#[serde(rename_all = "camelCase")]
2679#[non_exhaustive]
2680pub struct RequestPermissionRequest {
2681 pub session_id: SessionId,
2683 pub title: String,
2688 #[serde_as(deserialize_as = "DefaultOnError")]
2694 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2695 #[serde(default)]
2696 pub description: Option<String>,
2697 #[serde(default)]
2701 pub subject: Option<RequestPermissionSubject>,
2702 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
2705 pub options: Vec<PermissionOption>,
2706 #[serde_as(deserialize_as = "DefaultOnError")]
2712 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2713 #[serde(default)]
2714 #[serde(rename = "_meta")]
2715 pub meta: Option<Meta>,
2716}
2717
2718impl RequestPermissionRequest {
2719 #[must_use]
2721 pub fn new(
2722 session_id: impl Into<SessionId>,
2723 title: impl Into<String>,
2724 options: Vec<PermissionOption>,
2725 ) -> Self {
2726 Self {
2727 session_id: session_id.into(),
2728 title: title.into(),
2729 description: None,
2730 subject: None,
2731 options,
2732 meta: None,
2733 }
2734 }
2735
2736 #[must_use]
2738 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
2739 self.description = description.into_option();
2740 self
2741 }
2742
2743 #[must_use]
2745 pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
2746 self.subject = subject.into_option();
2747 self
2748 }
2749
2750 #[must_use]
2756 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2757 self.meta = meta.into_option();
2758 self
2759 }
2760}
2761
2762#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2764#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2765#[serde(tag = "type", rename_all = "snake_case")]
2766#[non_exhaustive]
2767pub enum RequestPermissionSubject {
2768 ToolCall(Box<ToolCallPermissionSubject>),
2770 Command(CommandPermissionSubject),
2772 #[serde(untagged)]
2783 Other(OtherRequestPermissionSubject),
2784}
2785
2786impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
2787 fn from(subject: ToolCallPermissionSubject) -> Self {
2788 Self::ToolCall(Box::new(subject))
2789 }
2790}
2791
2792impl From<ToolCallUpdate> for RequestPermissionSubject {
2793 fn from(tool_call: ToolCallUpdate) -> Self {
2794 ToolCallPermissionSubject::new(tool_call).into()
2795 }
2796}
2797
2798impl From<CommandPermissionSubject> for RequestPermissionSubject {
2799 fn from(subject: CommandPermissionSubject) -> Self {
2800 Self::Command(subject)
2801 }
2802}
2803
2804#[serde_as]
2806#[skip_serializing_none]
2807#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2808#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2809#[serde(rename_all = "camelCase")]
2810#[non_exhaustive]
2811pub struct ToolCallPermissionSubject {
2812 pub tool_call: ToolCallUpdate,
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 ToolCallPermissionSubject {
2831 #[must_use]
2833 pub fn new(tool_call: ToolCallUpdate) -> Self {
2834 Self {
2835 tool_call,
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#[serde_as]
2854#[skip_serializing_none]
2855#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2856#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2857#[serde(rename_all = "camelCase")]
2858#[non_exhaustive]
2859pub struct CommandPermissionSubject {
2860 pub command: String,
2862 pub cwd: AbsolutePath,
2864 #[serde_as(deserialize_as = "DefaultOnError")]
2866 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2867 #[serde(default)]
2868 pub tool_call_id: Option<ToolCallId>,
2869 #[serde_as(deserialize_as = "DefaultOnError")]
2871 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2872 #[serde(default)]
2873 pub terminal_id: Option<TerminalId>,
2874 #[serde_as(deserialize_as = "DefaultOnError")]
2880 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2881 #[serde(default)]
2882 #[serde(rename = "_meta")]
2883 pub meta: Option<Meta>,
2884}
2885
2886impl CommandPermissionSubject {
2887 #[must_use]
2889 pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
2890 Self {
2891 command: command.into(),
2892 cwd: cwd.into(),
2893 tool_call_id: None,
2894 terminal_id: None,
2895 meta: None,
2896 }
2897 }
2898
2899 #[must_use]
2901 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
2902 self.tool_call_id = tool_call_id.into_option();
2903 self
2904 }
2905
2906 #[must_use]
2908 pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
2909 self.terminal_id = terminal_id.into_option();
2910 self
2911 }
2912
2913 #[must_use]
2915 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2916 self.meta = meta.into_option();
2917 self
2918 }
2919}
2920
2921#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2923#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2924#[cfg_attr(feature = "schemars", schemars(inline))]
2925#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
2926#[serde(rename_all = "camelCase")]
2927#[non_exhaustive]
2928pub struct OtherRequestPermissionSubject {
2929 #[serde(rename = "type")]
2935 pub type_: String,
2936 #[serde(flatten)]
2938 pub fields: BTreeMap<String, serde_json::Value>,
2939}
2940
2941impl OtherRequestPermissionSubject {
2942 #[must_use]
2944 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2945 fields.remove("type");
2946 Self {
2947 type_: type_.into(),
2948 fields,
2949 }
2950 }
2951}
2952
2953impl<'de> Deserialize<'de> for OtherRequestPermissionSubject {
2954 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2955 where
2956 D: serde::Deserializer<'de>,
2957 {
2958 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2959 let type_ = fields
2960 .remove("type")
2961 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2962 let serde_json::Value::String(type_) = type_ else {
2963 return Err(serde::de::Error::custom("`type` must be a string"));
2964 };
2965
2966 if is_known_request_permission_subject_type(&type_) {
2967 return Err(serde::de::Error::custom(format!(
2968 "known request permission subject `{type_}` did not match its schema"
2969 )));
2970 }
2971
2972 Ok(Self { type_, fields })
2973 }
2974}
2975
2976fn is_known_request_permission_subject_type(type_: &str) -> bool {
2977 matches!(type_, "tool_call" | "command")
2978}
2979
2980#[cfg(feature = "schemars")]
2981fn other_request_permission_subject_schema(schema: &mut Schema) {
2982 super::schema_util::reject_known_string_discriminators(
2983 schema,
2984 "type",
2985 &["tool_call", "command"],
2986 );
2987}
2988
2989#[serde_as]
2991#[skip_serializing_none]
2992#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2993#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2994#[serde(rename_all = "camelCase")]
2995#[non_exhaustive]
2996pub struct PermissionOption {
2997 pub option_id: PermissionOptionId,
2999 pub name: String,
3001 pub kind: PermissionOptionKind,
3003 #[serde_as(deserialize_as = "DefaultOnError")]
3009 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3010 #[serde(default)]
3011 #[serde(rename = "_meta")]
3012 pub meta: Option<Meta>,
3013}
3014
3015impl PermissionOption {
3016 #[must_use]
3018 pub fn new(
3019 option_id: impl Into<PermissionOptionId>,
3020 name: impl Into<String>,
3021 kind: PermissionOptionKind,
3022 ) -> Self {
3023 Self {
3024 option_id: option_id.into(),
3025 name: name.into(),
3026 kind,
3027 meta: None,
3028 }
3029 }
3030
3031 #[must_use]
3037 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3038 self.meta = meta.into_option();
3039 self
3040 }
3041}
3042
3043#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3045#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
3046#[serde(transparent)]
3047#[from(forward)]
3048#[non_exhaustive]
3049pub struct PermissionOptionId(pub Arc<str>);
3050
3051impl PermissionOptionId {
3052 #[must_use]
3054 pub fn new(id: impl Into<Self>) -> Self {
3055 id.into()
3056 }
3057}
3058
3059#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3063#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3064#[serde(rename_all = "snake_case")]
3065#[non_exhaustive]
3066pub enum PermissionOptionKind {
3067 AllowOnce,
3069 AllowAlways,
3071 RejectOnce,
3073 RejectAlways,
3075 #[serde(untagged)]
3081 Other(String),
3082}
3083
3084#[serde_as]
3086#[skip_serializing_none]
3087#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3088#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3089#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
3090#[serde(rename_all = "camelCase")]
3091#[non_exhaustive]
3092pub struct RequestPermissionResponse {
3093 pub outcome: RequestPermissionOutcome,
3095 #[serde_as(deserialize_as = "DefaultOnError")]
3101 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3102 #[serde(default)]
3103 #[serde(rename = "_meta")]
3104 pub meta: Option<Meta>,
3105}
3106
3107impl RequestPermissionResponse {
3108 #[must_use]
3110 pub fn new(outcome: RequestPermissionOutcome) -> Self {
3111 Self {
3112 outcome,
3113 meta: None,
3114 }
3115 }
3116
3117 #[must_use]
3123 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3124 self.meta = meta.into_option();
3125 self
3126 }
3127}
3128
3129#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3132#[serde(tag = "outcome", rename_all = "snake_case")]
3133#[non_exhaustive]
3134pub enum RequestPermissionOutcome {
3135 Cancelled(CancelledPermissionOutcome),
3145 #[serde(rename_all = "camelCase")]
3147 Selected(SelectedPermissionOutcome),
3148 #[serde(untagged)]
3159 Other(OtherRequestPermissionOutcome),
3160}
3161
3162#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3168#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3169#[cfg_attr(feature = "schemars", schemars(inline))]
3170#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
3171#[serde(rename_all = "camelCase")]
3172#[non_exhaustive]
3173pub struct OtherRequestPermissionOutcome {
3174 pub outcome: String,
3180 #[serde(flatten)]
3182 pub fields: BTreeMap<String, serde_json::Value>,
3183}
3184
3185impl OtherRequestPermissionOutcome {
3186 #[must_use]
3188 pub fn new(
3189 outcome: impl Into<String>,
3190 mut fields: BTreeMap<String, serde_json::Value>,
3191 ) -> Self {
3192 fields.remove("outcome");
3193 Self {
3194 outcome: outcome.into(),
3195 fields,
3196 }
3197 }
3198}
3199
3200impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
3201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3202 where
3203 D: serde::Deserializer<'de>,
3204 {
3205 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
3206 let outcome = fields
3207 .remove("outcome")
3208 .ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
3209 let serde_json::Value::String(outcome) = outcome else {
3210 return Err(serde::de::Error::custom("`outcome` must be a string"));
3211 };
3212
3213 if is_known_request_permission_outcome(&outcome) {
3214 return Err(serde::de::Error::custom(format!(
3215 "known request permission outcome `{outcome}` did not match its schema"
3216 )));
3217 }
3218
3219 Ok(Self { outcome, fields })
3220 }
3221}
3222
3223fn is_known_request_permission_outcome(outcome: &str) -> bool {
3224 matches!(outcome, "cancelled" | "selected")
3225}
3226
3227#[cfg(feature = "schemars")]
3228fn other_request_permission_outcome_schema(schema: &mut Schema) {
3229 super::schema_util::reject_known_string_discriminators(
3230 schema,
3231 "outcome",
3232 &["cancelled", "selected"],
3233 );
3234}
3235
3236#[serde_as]
3238#[skip_serializing_none]
3239#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3240#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3241#[serde(rename_all = "camelCase")]
3242#[non_exhaustive]
3243pub struct CancelledPermissionOutcome {
3244 #[serde_as(deserialize_as = "DefaultOnError")]
3250 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3251 #[serde(default)]
3252 #[serde(rename = "_meta")]
3253 pub meta: Option<Meta>,
3254}
3255
3256impl CancelledPermissionOutcome {
3257 #[must_use]
3259 pub fn new() -> Self {
3260 Self::default()
3261 }
3262
3263 #[must_use]
3269 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3270 self.meta = meta.into_option();
3271 self
3272 }
3273}
3274
3275impl From<CancelledPermissionOutcome> for RequestPermissionOutcome {
3276 fn from(outcome: CancelledPermissionOutcome) -> Self {
3277 Self::Cancelled(outcome)
3278 }
3279}
3280
3281#[serde_as]
3283#[skip_serializing_none]
3284#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3286#[serde(rename_all = "camelCase")]
3287#[non_exhaustive]
3288pub struct SelectedPermissionOutcome {
3289 pub option_id: PermissionOptionId,
3291 #[serde_as(deserialize_as = "DefaultOnError")]
3297 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3298 #[serde(default)]
3299 #[serde(rename = "_meta")]
3300 pub meta: Option<Meta>,
3301}
3302
3303impl SelectedPermissionOutcome {
3304 #[must_use]
3306 pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
3307 Self {
3308 option_id: option_id.into(),
3309 meta: None,
3310 }
3311 }
3312
3313 #[must_use]
3319 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3320 self.meta = meta.into_option();
3321 self
3322 }
3323}
3324
3325#[serde_as]
3334#[skip_serializing_none]
3335#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3336#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3337#[serde(rename_all = "camelCase")]
3338#[non_exhaustive]
3339pub struct ClientCapabilities {
3340 #[serde_as(deserialize_as = "DefaultOnError")]
3347 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3348 #[serde(default)]
3349 pub auth: Option<ClientAuthCapabilities>,
3350 #[serde_as(deserialize_as = "DefaultOnError")]
3356 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3357 #[serde(default)]
3358 pub elicitation: Option<ElicitationCapabilities>,
3359
3360 #[serde_as(deserialize_as = "DefaultOnError")]
3366 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3367 #[serde(default)]
3368 #[serde(rename = "_meta")]
3369 pub meta: Option<Meta>,
3370}
3371
3372impl ClientCapabilities {
3373 #[must_use]
3375 pub fn new() -> Self {
3376 Self::default()
3377 }
3378
3379 #[must_use]
3383 pub fn auth(mut self, auth: impl IntoOption<ClientAuthCapabilities>) -> Self {
3384 self.auth = auth.into_option();
3385 self
3386 }
3387
3388 #[must_use]
3391 pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
3392 self.elicitation = elicitation.into_option();
3393 self
3394 }
3395
3396 #[must_use]
3402 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3403 self.meta = meta.into_option();
3404 self
3405 }
3406}
3407
3408#[serde_as]
3414#[skip_serializing_none]
3415#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3416#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3417#[serde(rename_all = "camelCase")]
3418#[non_exhaustive]
3419pub struct ClientAuthCapabilities {
3420 #[serde_as(deserialize_as = "DefaultOnError")]
3427 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3428 #[serde(default)]
3429 pub terminal: Option<TerminalAuthCapabilities>,
3430 #[serde_as(deserialize_as = "DefaultOnError")]
3436 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3437 #[serde(default)]
3438 #[serde(rename = "_meta")]
3439 pub meta: Option<Meta>,
3440}
3441
3442impl ClientAuthCapabilities {
3443 #[must_use]
3445 pub fn new() -> Self {
3446 Self::default()
3447 }
3448
3449 #[must_use]
3457 pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
3458 self.terminal = terminal.into_option();
3459 self
3460 }
3461
3462 #[must_use]
3468 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3469 self.meta = meta.into_option();
3470 self
3471 }
3472}
3473
3474#[serde_as]
3480#[skip_serializing_none]
3481#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3482#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3483#[non_exhaustive]
3484pub struct TerminalAuthCapabilities {
3485 #[serde_as(deserialize_as = "DefaultOnError")]
3491 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3492 #[serde(default)]
3493 #[serde(rename = "_meta")]
3494 pub meta: Option<Meta>,
3495}
3496
3497impl TerminalAuthCapabilities {
3498 #[must_use]
3500 pub fn new() -> Self {
3501 Self::default()
3502 }
3503
3504 #[must_use]
3510 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3511 self.meta = meta.into_option();
3512 self
3513 }
3514}
3515
3516#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3522#[non_exhaustive]
3523pub struct ClientMethodNames {
3524 pub session_request_permission: &'static str,
3526 pub session_update: &'static str,
3528 #[cfg(feature = "unstable_mcp_over_acp")]
3530 pub mcp_message: &'static str,
3531 pub elicitation_create: &'static str,
3533 pub elicitation_complete: &'static str,
3535}
3536
3537pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
3539 session_update: SESSION_UPDATE_NOTIFICATION,
3540 session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
3541 #[cfg(feature = "unstable_mcp_over_acp")]
3542 mcp_message: MCP_MESSAGE_METHOD_NAME,
3543 elicitation_create: ELICITATION_CREATE_METHOD_NAME,
3544 elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
3545};
3546
3547pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
3549pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
3551pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
3553pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
3555
3556#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3563#[derive(Clone, Debug, Serialize, Deserialize)]
3564#[serde(untagged)]
3565#[cfg_attr(feature = "schemars", schemars(inline))]
3566#[non_exhaustive]
3567pub enum AgentRequest {
3568 RequestPermissionRequest(Box<RequestPermissionRequest>),
3579 CreateElicitationRequest(Box<CreateElicitationRequest>),
3583 #[cfg(feature = "unstable_mcp_over_acp")]
3589 MessageMcpRequest(Box<MessageMcpRequest>),
3590 ExtMethodRequest(Box<ExtRequest>),
3598}
3599
3600impl AgentRequest {
3601 #[must_use]
3603 pub fn method(&self) -> &str {
3604 match self {
3605 Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
3606 Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
3607 #[cfg(feature = "unstable_mcp_over_acp")]
3608 Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
3609 Self::ExtMethodRequest(ext_request) => &ext_request.method,
3610 }
3611 }
3612}
3613
3614#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3621#[derive(Clone, Debug, Serialize, Deserialize)]
3622#[serde(untagged)]
3623#[cfg_attr(feature = "schemars", schemars(inline))]
3624#[non_exhaustive]
3625pub enum ClientResponse {
3626 RequestPermissionResponse(Box<RequestPermissionResponse>),
3628 CreateElicitationResponse(Box<CreateElicitationResponse>),
3630 #[cfg(feature = "unstable_mcp_over_acp")]
3632 MessageMcpResponse(Box<MessageMcpResponse>),
3633 ExtMethodResponse(Box<ExtResponse>),
3635}
3636
3637#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3644#[derive(Clone, Debug, Serialize, Deserialize)]
3645#[serde(untagged)]
3646#[cfg_attr(feature = "schemars", schemars(inline))]
3647#[non_exhaustive]
3648pub enum AgentNotification {
3649 UpdateSessionNotification(Box<UpdateSessionNotification>),
3662 CompleteElicitationNotification(Box<CompleteElicitationNotification>),
3666 ExtNotification(Box<ExtNotification>),
3674}
3675
3676impl AgentNotification {
3677 #[must_use]
3679 pub fn method(&self) -> &str {
3680 match self {
3681 Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
3682 Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
3683 Self::ExtNotification(ext_notification) => &ext_notification.method,
3684 }
3685 }
3686}
3687
3688#[cfg(test)]
3689mod tests {
3690
3691 #[test]
3692 fn replacement_lists_reject_non_arrays_instead_of_clearing() {
3693 use serde_json::json;
3694 for (update, key) in [
3695 ("config_option_update", "configOptions"),
3696 ("available_commands_update", "availableCommands"),
3697 ] {
3698 let empty = json!({ "sessionUpdate": update, key: [] });
3699 serde_json::from_value::<super::SessionUpdate>(empty).unwrap();
3700 for bad in [json!("oops"), json!(null), json!({})] {
3701 let wire = json!({ "sessionUpdate": update, key: bad });
3702 assert!(
3703 serde_json::from_value::<super::SessionUpdate>(wire).is_err(),
3704 "{update} {bad}"
3705 );
3706 }
3707 }
3708 let plan = |entries: serde_json::Value| {
3709 json!({ "sessionUpdate": "plan_update",
3710 "plan": { "type": "items", "planId": "p", "entries": entries } })
3711 };
3712 serde_json::from_value::<super::SessionUpdate>(plan(json!([]))).unwrap();
3713 assert!(serde_json::from_value::<super::SessionUpdate>(plan(json!("oops"))).is_err());
3714 let skipped =
3716 plan(json!([42, { "content": "a", "priority": "high", "status": "pending" }]));
3717 let super::SessionUpdate::PlanUpdate(update) =
3718 serde_json::from_value::<super::SessionUpdate>(skipped).unwrap()
3719 else {
3720 panic!("expected plan_update");
3721 };
3722 let crate::v2::PlanUpdateContent::Items(items) = update.plan else {
3723 panic!("expected items");
3724 };
3725 assert_eq!(items.entries.len(), 1);
3726 }
3727
3728 #[test]
3729 fn other_available_command_preserves_meta_verbatim() {
3730 for meta in [serde_json::json!({ "k": 1 }), serde_json::json!("bad")] {
3731 let wire = serde_json::json!({ "type": "_custom", "name": "c",
3732 "description": "d", "_meta": meta, "extra": true });
3733 let command: super::AvailableCommand = serde_json::from_value(wire.clone()).unwrap();
3734 assert_eq!(command.meta(), meta.as_object());
3735 assert_eq!(serde_json::to_value(&command).unwrap(), wire);
3736 }
3737 }
3738
3739 #[test]
3740 fn cancelled_permission_outcome_wire_format() {
3741 let outcome: RequestPermissionOutcome =
3742 serde_json::from_value(serde_json::json!({ "outcome": "cancelled" })).unwrap();
3743 assert_eq!(outcome, CancelledPermissionOutcome::new().into());
3744 assert_eq!(
3745 serde_json::to_value(&outcome).unwrap(),
3746 serde_json::json!({ "outcome": "cancelled" })
3747 );
3748
3749 let with_meta: RequestPermissionOutcome = CancelledPermissionOutcome::new()
3750 .meta(serde_json::Map::from_iter([(
3751 "k".to_string(),
3752 serde_json::json!(1),
3753 )]))
3754 .into();
3755 let json = serde_json::to_value(&with_meta).unwrap();
3756 assert_eq!(
3757 json,
3758 serde_json::json!({ "outcome": "cancelled", "_meta": { "k": 1 } })
3759 );
3760 assert_eq!(
3761 serde_json::from_value::<RequestPermissionOutcome>(json).unwrap(),
3762 with_meta
3763 );
3764 }
3765
3766 use super::*;
3767
3768 #[cfg(feature = "unstable_subagents")]
3769 #[test]
3770 fn subagent_update_serializes_as_upsert() {
3771 use serde_json::json;
3772
3773 let announced =
3774 SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
3775 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
3776 ));
3777 let wire = json!({
3778 "sessionUpdate": "subagent_update",
3779 "sessionId": "sess_child_1",
3780 "capabilities": { "cancel": {} }
3781 });
3782 assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
3783 assert_eq!(
3784 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
3785 announced
3786 );
3787
3788 let minimal = SubagentUpdate::new("sess_child_1");
3789 assert_eq!(
3790 serde_json::to_value(&minimal).unwrap(),
3791 json!({
3792 "sessionId": "sess_child_1"
3793 })
3794 );
3795 assert!(minimal.capabilities.is_undefined());
3796 assert!(minimal.state.is_undefined());
3797 assert!(minimal.meta.is_undefined());
3798
3799 let cleared_wire = json!({
3801 "sessionId": "sess_child_1",
3802 "capabilities": null,
3803 "state": null,
3804 "_meta": null
3805 });
3806 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3807 assert!(cleared.capabilities.is_null());
3808 assert!(cleared.state.is_null());
3809 assert!(cleared.meta.is_null());
3810 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3811
3812 let revoked: SubagentUpdate = serde_json::from_value(json!({
3813 "sessionId": "sess_child_1",
3814 "capabilities": {}
3815 }))
3816 .unwrap();
3817 assert_eq!(
3818 revoked.capabilities,
3819 MaybeUndefined::Value(SubagentSessionCapabilities::new())
3820 );
3821 assert!(
3822 matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
3823 );
3824
3825 assert!(
3826 serde_json::from_value::<SessionUpdate>(json!({
3827 "sessionUpdate": "subagent_update"
3828 }))
3829 .is_err()
3830 );
3831 }
3832
3833 #[cfg(feature = "unstable_subagents")]
3834 #[test]
3835 fn subagent_cancel_capability_is_optional_object() {
3836 use serde_json::json;
3837
3838 for wire in [
3839 json!({}),
3840 json!({"cancel": null}),
3841 json!({"cancel": true}),
3842 json!({"cancel": false}),
3843 ] {
3844 let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
3845 assert!(child.cancel.is_none());
3846 assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
3847 }
3848 let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
3849 assert_eq!(
3850 serde_json::to_value(&enabled).unwrap(),
3851 json!({"cancel": {}})
3852 );
3853 assert_eq!(
3854 serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
3855 enabled
3856 );
3857 let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
3858 let with_meta =
3859 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
3860 let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
3861 assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
3862 assert_eq!(
3863 serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
3864 with_meta
3865 );
3866 assert!(enabled.cancel.is_some());
3867 assert!(enabled.cancel(None).cancel.is_none());
3868 let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
3869 assert_eq!(
3870 serde_json::to_value(removed).unwrap(),
3871 json!({"sessionId": "child", "capabilities": {}})
3872 );
3873 }
3874
3875 #[cfg(feature = "unstable_subagents")]
3876 #[test]
3877 fn subagent_display_metadata_uses_patch_semantics() {
3878 use serde_json::json;
3879
3880 let update = SubagentUpdate::new("child")
3881 .title("Test investigator".to_string())
3882 .description("Investigates platform-specific test failures.".to_string());
3883 let wire = json!({
3884 "sessionId": "child",
3885 "title": "Test investigator",
3886 "description": "Investigates platform-specific test failures."
3887 });
3888 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3889 assert_eq!(
3890 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
3891 update
3892 );
3893
3894 let minimal = SubagentUpdate::new("child");
3895 assert!(minimal.title.is_undefined());
3896 assert!(minimal.description.is_undefined());
3897 assert_eq!(
3898 serde_json::to_value(&minimal).unwrap(),
3899 json!({"sessionId": "child"})
3900 );
3901 let cleared_wire = json!({
3902 "sessionId": "child", "title": null, "description": null
3903 });
3904 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3905 assert!(cleared.title.is_null());
3906 assert!(cleared.description.is_null());
3907 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3908
3909 let title_only: SubagentUpdate = serde_json::from_value(json!({
3910 "sessionId": "child", "title": "Updated title"
3911 }))
3912 .unwrap();
3913 assert_eq!(
3914 title_only.title,
3915 MaybeUndefined::Value("Updated title".to_string())
3916 );
3917 assert!(title_only.description.is_undefined());
3918 let malformed: SubagentUpdate = serde_json::from_value(json!({
3919 "sessionId": "child", "title": false, "description": false
3920 }))
3921 .unwrap();
3922 assert_eq!(malformed, minimal);
3923 }
3924
3925 #[cfg(feature = "unstable_subagents")]
3926 #[test]
3927 fn subagent_notification_keeps_parent_and_child_ids_nested() {
3928 use serde_json::json;
3929
3930 let notification = UpdateSessionNotification::new(
3931 "parent",
3932 SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
3933 );
3934 let wire = json!({
3935 "sessionId": "parent",
3936 "update": {
3937 "sessionUpdate": "subagent_update",
3938 "sessionId": "child"
3939 }
3940 });
3941 assert_eq!(serde_json::to_value(¬ification).unwrap(), wire);
3942 assert_eq!(
3943 serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
3944 notification
3945 );
3946 for malformed in [
3947 json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
3948 json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
3949 ] {
3950 assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
3951 }
3952 }
3953
3954 #[cfg(feature = "unstable_subagents")]
3955 #[test]
3956 fn subagent_mirrors_normal_child_state_notifications() {
3957 use serde_json::json;
3958
3959 let association = SubagentUpdate::new("sess_child");
3960 for state in [
3961 StateUpdate::Running(RunningStateUpdate::new()),
3962 StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
3963 StateUpdate::Unknown(UnknownStateUpdate::new()),
3964 StateUpdate::Running(RunningStateUpdate::new()),
3965 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
3966 StateUpdate::Running(RunningStateUpdate::new()),
3967 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
3968 ] {
3969 let child_notification = UpdateSessionNotification::new(
3970 association.session_id.clone(),
3971 SessionUpdate::StateUpdate(state.clone()),
3972 );
3973 let parent_notification = UpdateSessionNotification::new(
3974 "sess_parent",
3975 SessionUpdate::SubagentUpdate(association.clone().state(state)),
3976 );
3977 let child_wire = serde_json::to_value(&child_notification).unwrap();
3978 let parent_wire = serde_json::to_value(&parent_notification).unwrap();
3979 assert_eq!(child_wire["sessionId"], json!("sess_child"));
3980 assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
3981 assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
3982 assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
3983 let mut child_snapshot = child_wire["update"].clone();
3984 child_snapshot
3985 .as_object_mut()
3986 .unwrap()
3987 .remove("sessionUpdate");
3988 assert_eq!(parent_wire["update"]["state"], child_snapshot);
3989 assert_eq!(
3990 serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
3991 child_notification
3992 );
3993 assert_eq!(
3994 serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
3995 parent_notification
3996 );
3997 }
3998 }
3999
4000 #[cfg(feature = "unstable_subagents")]
4001 #[test]
4002 fn subagent_state_uses_whole_snapshot_patch_semantics() {
4003 use serde_json::json;
4004
4005 let minimal = SubagentUpdate::new("child");
4006 assert!(minimal.state.is_undefined());
4007 assert_eq!(
4008 serde_json::to_value(&minimal).unwrap(),
4009 json!({"sessionId": "child"})
4010 );
4011
4012 let cleared = minimal.clone().state(None::<StateUpdate>);
4013 assert!(cleared.state.is_null());
4014 assert_eq!(
4015 serde_json::to_value(&cleared).unwrap(),
4016 json!({"sessionId": "child", "state": null})
4017 );
4018 assert_eq!(
4019 serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
4020 .unwrap(),
4021 cleared
4022 );
4023
4024 for snapshot in [
4025 json!({"state": "running", "_meta": {"source": "worker"}}),
4026 json!({"state": "idle", "stopReason": "end_turn"}),
4027 json!({"state": "requires_action"}),
4028 json!({"state": "unknown"}),
4029 json!({"state": "_custom", "detail": {"phase": 2}}),
4030 ] {
4031 let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
4032 let update = minimal.clone().state(state.clone());
4033 let wire = json!({"sessionId": "child", "state": snapshot});
4034 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4035 assert_eq!(
4036 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4037 update
4038 );
4039 assert_eq!(update.state, MaybeUndefined::Value(state));
4040 }
4041
4042 let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
4045 IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
4046 ));
4047 let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
4048 assert_eq!(
4049 serde_json::to_value(running).unwrap(),
4050 json!({"sessionId": "child", "state": {"state": "running"}})
4051 );
4052 for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
4053 let parsed: SubagentUpdate =
4054 serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
4055 assert_eq!(parsed, minimal);
4056 }
4057 }
4058
4059 #[cfg(feature = "unstable_subagents")]
4060 #[test]
4061 fn unknown_activity_is_a_known_state_update() {
4062 use serde_json::json;
4063
4064 let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
4065 UnknownStateUpdate::new().meta(
4066 [("source".into(), json!("worker"))]
4067 .into_iter()
4068 .collect::<Meta>(),
4069 ),
4070 ));
4071 let wire = json!({
4072 "sessionUpdate": "state_update",
4073 "state": "unknown",
4074 "_meta": { "source": "worker" }
4075 });
4076 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4077 assert_eq!(
4078 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4079 update
4080 );
4081
4082 for meta in [json!(null), json!(false)] {
4083 let state: StateUpdate = serde_json::from_value(json!({
4084 "state": "unknown",
4085 "_meta": meta
4086 }))
4087 .unwrap();
4088 assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
4089 assert_eq!(
4090 serde_json::to_value(state).unwrap(),
4091 json!({"state": "unknown"})
4092 );
4093 }
4094 assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
4095 }
4096
4097 #[cfg(not(feature = "unstable_subagents"))]
4098 #[test]
4099 fn unknown_activity_is_preserved_without_subagents_feature() {
4100 let wire = serde_json::json!({
4101 "sessionUpdate": "state_update",
4102 "state": "unknown",
4103 "_meta": { "source": "worker" }
4104 });
4105 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4106 let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
4107 panic!("expected unrecognized state payload");
4108 };
4109 assert_eq!(state.state, "unknown");
4110 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4111 }
4112
4113 #[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
4114 #[test]
4115 fn subagent_schema_carries_optional_nullable_state_snapshot() {
4116 use serde_json::json;
4117
4118 let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
4119 let properties = schema["properties"].as_object().unwrap();
4120 assert!(properties.contains_key("sessionId"));
4121 assert!(properties.contains_key("title"));
4122 assert!(properties.contains_key("description"));
4123 assert!(properties.contains_key("capabilities"));
4124 assert!(properties.contains_key("state"));
4125 assert!(properties.contains_key("_meta"));
4126 assert!(!properties.contains_key("name"));
4127 assert!(!properties.contains_key("task"));
4128 assert_eq!(schema["required"], json!(["sessionId"]));
4129 assert_eq!(
4130 properties["state"]["x-deserialize-default-on-error"],
4131 json!(true)
4132 );
4133 let state_variants = properties["state"]["anyOf"].as_array().unwrap();
4134 assert!(state_variants.contains(&json!({"type": "null"})));
4135 assert!(
4136 state_variants
4137 .iter()
4138 .any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
4139 );
4140 let child =
4141 serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
4142 let variants = child["properties"]["cancel"]["anyOf"]
4143 .as_array()
4144 .expect("cancel must allow the capability object or null");
4145 assert!(variants.contains(&json!({"type": "null"})));
4146 assert!(
4147 variants
4148 .iter()
4149 .any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
4150 );
4151 assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
4152 let cancel =
4153 serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
4154 assert_eq!(cancel["type"], "object");
4155 assert!(
4156 !child["required"]
4157 .as_array()
4158 .is_some_and(|required| required.contains(&json!("cancel")))
4159 );
4160 }
4161
4162 #[cfg(not(feature = "unstable_subagents"))]
4163 #[test]
4164 fn unsupported_subagent_update_is_preserved() {
4165 let wire = serde_json::json!({
4166 "sessionUpdate": "subagent_update",
4167 "sessionId": "sess_child",
4168 "capabilities": { "cancel": {} }
4169 });
4170 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4171 assert!(matches!(&parsed, SessionUpdate::Other(_)));
4172 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4173 }
4174
4175 #[test]
4176 fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
4177 use serde_json::json;
4178
4179 let mut meta = Meta::new();
4180 meta.insert("source".into(), json!("fallback"));
4181 let v2_notice = SessionUpdate::Notice(
4182 Notice::new(NoticeSeverity::Error, "Provider degraded")
4183 .description("Requests may take longer than usual.")
4184 .meta(meta.clone()),
4185 );
4186 let expected = json!({
4187 "sessionUpdate": "notice",
4188 "severity": "error",
4189 "title": "Provider degraded",
4190 "description": "Requests may take longer than usual.",
4191 "_meta": { "source": "fallback" }
4192 });
4193 assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
4194
4195 let v1_notice = crate::v1::SessionUpdate::Notice(
4196 crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
4197 .description("Requests may take longer than usual.")
4198 .meta(meta),
4199 );
4200 assert_eq!(
4201 serde_json::to_value(v2_notice).unwrap(),
4202 serde_json::to_value(v1_notice).unwrap()
4203 );
4204
4205 let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
4206 "sessionUpdate": "notice",
4207 "severity": "critical",
4208 "title": "Provider degraded",
4209 "description": null,
4210 "_meta": null
4211 }))
4212 .unwrap() else {
4213 panic!("expected notice");
4214 };
4215
4216 assert_eq!(
4217 notice.severity,
4218 NoticeSeverity::Other("critical".to_string())
4219 );
4220 assert_eq!(notice.description, None);
4221 assert_eq!(notice.meta, None);
4222 assert_eq!(
4223 serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
4224 json!({
4225 "sessionUpdate": "notice",
4226 "severity": "critical",
4227 "title": "Provider degraded"
4228 })
4229 );
4230 }
4231
4232 #[test]
4233 fn malformed_known_notice_is_not_hidden_as_unknown() {
4234 use serde_json::json;
4235
4236 for malformed in [
4237 json!({
4238 "sessionUpdate": "notice",
4239 "severity": "warning",
4240 "title": 42
4241 }),
4242 json!({
4243 "sessionUpdate": "notice",
4244 "severity": 42,
4245 "title": "MCP server unavailable"
4246 }),
4247 json!({
4248 "sessionUpdate": "notice",
4249 "severity": "warning"
4250 }),
4251 json!({
4252 "sessionUpdate": "notice",
4253 "severity": "warning",
4254 "title": null
4255 }),
4256 json!({
4257 "sessionUpdate": "notice",
4258 "title": "MCP server unavailable"
4259 }),
4260 json!({
4261 "sessionUpdate": "notice",
4262 "severity": null,
4263 "title": "MCP server unavailable"
4264 }),
4265 ] {
4266 assert!(serde_json::from_str::<SessionUpdate>(&malformed.to_string()).is_err());
4267 assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
4268 }
4269 }
4270
4271 #[test]
4272 fn notice_tolerates_invalid_optional_display_fields_and_custom_severity() {
4273 use serde_json::json;
4274
4275 let wire = json!({
4276 "sessionUpdate": "notice",
4277 "severity": "_custom",
4278 "title": "Provider degraded",
4279 "description": 42,
4280 "_meta": false
4281 });
4282 let SessionUpdate::Notice(notice) = serde_json::from_value(wire.clone()).unwrap() else {
4283 panic!("expected notice");
4284 };
4285 assert_eq!(
4286 serde_json::from_str::<SessionUpdate>(&wire.to_string()).unwrap(),
4287 SessionUpdate::Notice(notice.clone())
4288 );
4289 assert_eq!(notice.severity, NoticeSeverity::Other("_custom".into()));
4290 assert_eq!(notice.description, None);
4291 assert_eq!(notice.meta, None);
4292 assert_eq!(
4293 serde_json::to_value(notice).unwrap(),
4294 json!({"severity": "_custom", "title": "Provider degraded"})
4295 );
4296 }
4297
4298 #[cfg(feature = "schemars")]
4299 #[test]
4300 fn notice_schema_preserves_required_fields_and_excludes_unknown_fallback() {
4301 use serde_json::json;
4302
4303 let schema = serde_json::to_value(schemars::schema_for!(Notice)).unwrap();
4304 assert_eq!(schema["required"], json!(["severity", "title"]));
4305 assert_eq!(schema["properties"]["title"]["minLength"], 1);
4306 for field in ["description", "_meta"] {
4307 assert!(
4308 schema["properties"][field]["type"]
4309 .as_array()
4310 .unwrap()
4311 .contains(&json!("null"))
4312 );
4313 }
4314 let fallback = serde_json::to_value(schemars::schema_for!(OtherSessionUpdate)).unwrap();
4315 assert!(
4316 fallback["not"]["anyOf"]
4317 .as_array()
4318 .unwrap()
4319 .iter()
4320 .any(|variant| variant["properties"]["sessionUpdate"]["const"] == "notice")
4321 );
4322 }
4323
4324 #[test]
4325 fn compaction_updates_preserve_patch_and_open_status_semantics() {
4326 use serde_json::json;
4327
4328 assert_eq!(
4329 serde_json::to_value(SessionUpdate::CompactionUpdate(
4330 CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
4331 ContentBlock::Text(crate::v2::TextContent::new("retained")),
4332 ]),
4333 ))
4334 .unwrap(),
4335 json!({
4336 "sessionUpdate": "compaction_update",
4337 "compactionId": "cmp_001",
4338 "status": "completed",
4339 "summary": [{ "type": "text", "text": "retained" }]
4340 })
4341 );
4342
4343 let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
4344 "sessionUpdate": "compaction_update",
4345 "compactionId": "cmp_001",
4346 "status": "paused",
4347 "summary": null
4348 }))
4349 .unwrap() else {
4350 panic!("expected compaction update");
4351 };
4352 assert_eq!(update.status, CompactionStatus::Other("paused".into()));
4353 assert!(update.summary.is_null());
4354 assert!(update.error.is_undefined());
4355 }
4356
4357 #[test]
4358 fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
4359 use serde_json::json;
4360
4361 assert!(
4362 serde_json::from_value::<SessionUpdate>(json!({
4363 "sessionUpdate": "compaction_update",
4364 "status": "completed"
4365 }))
4366 .is_err()
4367 );
4368 assert!(
4369 serde_json::from_value::<SessionUpdate>(json!({
4370 "sessionUpdate": "compaction_summary_chunk",
4371 "compactionId": "cmp_001"
4372 }))
4373 .is_err()
4374 );
4375 }
4376
4377 #[test]
4378 fn test_elicitation_capability_semantics() {
4379 use serde_json::json;
4380
4381 let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
4382 assert!(unsupported.elicitation.is_none());
4383
4384 let null: ClientCapabilities =
4385 serde_json::from_value(json!({ "elicitation": null })).unwrap();
4386 assert!(null.elicitation.is_none());
4387
4388 let malformed: ClientCapabilities =
4389 serde_json::from_value(json!({ "elicitation": false })).unwrap();
4390 assert!(malformed.elicitation.is_none());
4391
4392 let empty: ClientCapabilities =
4393 serde_json::from_value(json!({ "elicitation": {} })).unwrap();
4394 let empty = empty.elicitation.expect("present capability");
4395 assert!(!empty.supports_form());
4396 assert!(!empty.supports_url());
4397
4398 let form_only: ClientCapabilities = serde_json::from_value(json!({
4399 "elicitation": { "form": {} }
4400 }))
4401 .unwrap();
4402 let form_only = form_only.elicitation.expect("advertised capability");
4403 assert!(form_only.supports_form());
4404 assert!(!form_only.supports_url());
4405
4406 let url_only: ClientCapabilities = serde_json::from_value(json!({
4407 "elicitation": { "url": {} }
4408 }))
4409 .unwrap();
4410 let url_only = url_only.elicitation.expect("advertised capability");
4411 assert!(!url_only.supports_form());
4412 assert!(url_only.supports_url());
4413
4414 let both: ClientCapabilities = serde_json::from_value(json!({
4415 "elicitation": { "form": {}, "url": {} }
4416 }))
4417 .unwrap();
4418 let both = both.elicitation.expect("advertised capability");
4419 assert!(both.supports_form());
4420 assert!(both.supports_url());
4421 }
4422
4423 #[test]
4424 fn test_elicitation_method_routing_and_envelopes() {
4425 use serde_json::json;
4426
4427 assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
4428 assert_eq!(
4429 CLIENT_METHOD_NAMES.elicitation_complete,
4430 "elicitation/complete"
4431 );
4432
4433 let request =
4434 AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
4435 crate::v2::ElicitationFormMode::new(
4436 crate::v2::ElicitationSessionScope::new("sess_1"),
4437 crate::v2::ElicitationSchema::new(),
4438 ),
4439 "Choose a value",
4440 )));
4441 assert_eq!(request.method(), "elicitation/create");
4442 let method = Arc::from(request.method());
4443 let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
4444 id: crate::v2::RequestId::Number(7),
4445 method,
4446 params: Some(request),
4447 });
4448 assert_eq!(
4449 serde_json::to_value(request).unwrap(),
4450 json!({
4451 "jsonrpc": "2.0",
4452 "id": 7,
4453 "method": "elicitation/create",
4454 "params": {
4455 "mode": "form",
4456 "sessionId": "sess_1",
4457 "message": "Choose a value",
4458 "requestedSchema": { "type": "object", "properties": {} }
4459 }
4460 })
4461 );
4462
4463 let notification = AgentNotification::CompleteElicitationNotification(Box::new(
4464 CompleteElicitationNotification::new("elic_1"),
4465 ));
4466 assert_eq!(notification.method(), "elicitation/complete");
4467 let method = Arc::from(notification.method());
4468 let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
4469 method,
4470 params: Some(notification),
4471 });
4472 assert_eq!(
4473 serde_json::to_value(notification).unwrap(),
4474 json!({
4475 "jsonrpc": "2.0",
4476 "method": "elicitation/complete",
4477 "params": { "elicitationId": "elic_1" }
4478 })
4479 );
4480 }
4481
4482 #[test]
4483 fn test_client_capabilities_auth_defaults_on_malformed_value() {
4484 use serde_json::json;
4485
4486 let capabilities: ClientCapabilities = serde_json::from_value(json!({
4487 "auth": false
4488 }))
4489 .unwrap();
4490
4491 assert_eq!(capabilities.auth, None);
4492 }
4493
4494 #[test]
4495 fn test_serialization_behavior() {
4496 use serde_json::json;
4497
4498 assert_eq!(
4499 serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
4500 SessionInfoUpdate {
4501 title: MaybeUndefined::Undefined,
4502 updated_at: MaybeUndefined::Undefined,
4503 meta: MaybeUndefined::Undefined
4504 }
4505 );
4506 assert_eq!(
4507 serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
4508 .unwrap(),
4509 SessionInfoUpdate {
4510 title: MaybeUndefined::Null,
4511 updated_at: MaybeUndefined::Null,
4512 meta: MaybeUndefined::Undefined
4513 }
4514 );
4515 assert_eq!(
4516 serde_json::from_value::<SessionInfoUpdate>(
4517 json!({"title": "title", "updatedAt": "timestamp"})
4518 )
4519 .unwrap(),
4520 SessionInfoUpdate {
4521 title: MaybeUndefined::Value("title".to_string()),
4522 updated_at: MaybeUndefined::Value("timestamp".to_string()),
4523 meta: MaybeUndefined::Undefined
4524 }
4525 );
4526
4527 let clear_meta =
4528 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
4529 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4530
4531 let mut meta = Meta::new();
4532 meta.insert("source".to_string(), json!("session-info"));
4533
4534 assert_eq!(
4535 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
4536 "source": "session-info"
4537 }}))
4538 .unwrap()
4539 .meta,
4540 MaybeUndefined::Value(meta.clone())
4541 );
4542
4543 assert_eq!(
4544 serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
4545 json!({})
4546 );
4547
4548 assert_eq!(
4549 serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
4550 json!({"_meta": null})
4551 );
4552
4553 assert_eq!(
4554 serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
4555 json!({"_meta": {
4556 "source": "session-info"
4557 }})
4558 );
4559 assert_eq!(
4560 serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
4561 json!({"title": "title"})
4562 );
4563 assert_eq!(
4564 serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
4565 json!({"title": null})
4566 );
4567 assert_eq!(
4568 serde_json::to_value(
4569 SessionInfoUpdate::new()
4570 .title("title")
4571 .title(MaybeUndefined::Undefined)
4572 )
4573 .unwrap(),
4574 json!({})
4575 );
4576 }
4577
4578 #[test]
4579 fn test_content_chunk_message_id_serialization() {
4580 use serde_json::json;
4581
4582 assert_eq!(
4583 serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
4584 ContentBlock::Text(crate::v2::TextContent::new("Hello")),
4585 "msg_agent_c42b9",
4586 )))
4587 .unwrap(),
4588 json!({
4589 "sessionUpdate": "agent_message_chunk",
4590 "messageId": "msg_agent_c42b9",
4591 "content": {
4592 "type": "text",
4593 "text": "Hello"
4594 }
4595 })
4596 );
4597
4598 let err = serde_json::from_value::<ContentChunk>(json!({
4599 "content": {
4600 "type": "text",
4601 "text": "Hello"
4602 }
4603 }))
4604 .unwrap_err();
4605
4606 assert!(err.to_string().contains("messageId"), "{err}");
4607 }
4608
4609 #[test]
4610 fn test_tool_call_content_chunk_serialization() {
4611 use serde_json::json;
4612
4613 assert_eq!(
4614 serde_json::to_value(SessionUpdate::ToolCallContentChunk(
4615 ToolCallContentChunk::new(
4616 "call_001",
4617 crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
4618 )
4619 ))
4620 .unwrap(),
4621 json!({
4622 "sessionUpdate": "tool_call_content_chunk",
4623 "toolCallId": "call_001",
4624 "content": {
4625 "type": "content",
4626 "content": {
4627 "type": "text",
4628 "text": "partial output"
4629 }
4630 }
4631 })
4632 );
4633
4634 let err = serde_json::from_value::<ToolCallContentChunk>(json!({
4635 "content": {
4636 "type": "content",
4637 "content": {
4638 "type": "text",
4639 "text": "partial output"
4640 }
4641 }
4642 }))
4643 .unwrap_err();
4644
4645 assert!(err.to_string().contains("toolCallId"), "{err}");
4646 }
4647
4648 #[test]
4649 fn test_full_message_serialization() {
4650 use serde_json::json;
4651
4652 assert_eq!(
4653 serde_json::to_value(SessionUpdate::UserMessage(
4654 UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
4655 crate::v2::TextContent::new("Hello")
4656 )])
4657 ))
4658 .unwrap(),
4659 json!({
4660 "sessionUpdate": "user_message",
4661 "messageId": "msg_user_8f7a1",
4662 "content": [
4663 {
4664 "type": "text",
4665 "text": "Hello"
4666 }
4667 ]
4668 })
4669 );
4670
4671 assert_eq!(
4672 serde_json::to_value(SessionUpdate::AgentMessage(
4673 AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
4674 crate::v2::TextContent::new("Hello")
4675 )])
4676 ))
4677 .unwrap(),
4678 json!({
4679 "sessionUpdate": "agent_message",
4680 "messageId": "msg_agent_c42b9",
4681 "content": [
4682 {
4683 "type": "text",
4684 "text": "Hello"
4685 }
4686 ]
4687 })
4688 );
4689
4690 assert_eq!(
4691 serde_json::to_value(SessionUpdate::AgentThought(
4692 AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
4693 crate::v2::TextContent::new("Need to inspect the call sites first.")
4694 )])
4695 ))
4696 .unwrap(),
4697 json!({
4698 "sessionUpdate": "agent_thought",
4699 "messageId": "msg_thought_a12",
4700 "content": [
4701 {
4702 "type": "text",
4703 "text": "Need to inspect the call sites first."
4704 }
4705 ]
4706 })
4707 );
4708 }
4709
4710 #[test]
4711 fn test_message_upsert_serialization() {
4712 use serde_json::json;
4713
4714 assert_eq!(
4715 serde_json::to_value(SessionUpdate::UserMessage(
4716 UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
4717 ))
4718 .unwrap(),
4719 json!({
4720 "sessionUpdate": "user_message",
4721 "messageId": "msg_empty",
4722 "content": []
4723 })
4724 );
4725
4726 let empty = serde_json::from_value::<UserMessage>(json!({
4727 "messageId": "msg_empty",
4728 "content": []
4729 }))
4730 .unwrap();
4731 assert!(matches!(
4732 empty.content,
4733 MaybeUndefined::Value(ref content) if content.is_empty()
4734 ));
4735
4736 let patch = serde_json::from_value::<AgentMessage>(json!({
4737 "messageId": "msg_agent_c42b9"
4738 }))
4739 .unwrap();
4740 assert_eq!(patch.content, MaybeUndefined::Undefined);
4741 assert_eq!(patch.meta, MaybeUndefined::Undefined);
4742
4743 let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
4744 "messageId": "msg_agent_c42b9",
4745 "_meta": false
4746 }))
4747 .unwrap();
4748 assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
4749
4750 let patch = serde_json::from_value::<AgentThought>(json!({
4751 "messageId": "msg_thought_a12"
4752 }))
4753 .unwrap();
4754 assert_eq!(patch.content, MaybeUndefined::Undefined);
4755
4756 let clear = serde_json::from_value::<UserMessage>(json!({
4757 "messageId": "msg_user_8f7a1",
4758 "content": null
4759 }))
4760 .unwrap();
4761 assert_eq!(clear.content, MaybeUndefined::Null);
4762
4763 let clear_meta = serde_json::from_value::<UserMessage>(json!({
4764 "messageId": "msg_user_8f7a1",
4765 "_meta": null
4766 }))
4767 .unwrap();
4768 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4769
4770 let mut meta = Meta::new();
4771 meta.insert("source".to_string(), json!("replay"));
4772
4773 assert_eq!(
4774 serde_json::to_value(SessionUpdate::UserMessage(
4775 UserMessage::new("msg_user_8f7a1").meta(meta)
4776 ))
4777 .unwrap(),
4778 json!({
4779 "sessionUpdate": "user_message",
4780 "messageId": "msg_user_8f7a1",
4781 "_meta": {
4782 "source": "replay"
4783 }
4784 })
4785 );
4786
4787 assert_eq!(
4788 serde_json::to_value(SessionUpdate::UserMessage(
4789 UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
4790 ))
4791 .unwrap(),
4792 json!({
4793 "sessionUpdate": "user_message",
4794 "messageId": "msg_user_8f7a1",
4795 "_meta": null
4796 })
4797 );
4798 }
4799
4800 #[test]
4801 fn test_usage_update_serialization() {
4802 use serde_json::json;
4803
4804 assert_eq!(
4805 serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
4806 53_000, 200_000
4807 )))
4808 .unwrap(),
4809 json!({
4810 "sessionUpdate": "usage_update",
4811 "used": 53000,
4812 "size": 200_000
4813 })
4814 );
4815
4816 assert_eq!(
4817 serde_json::to_value(SessionUpdate::UsageUpdate(
4818 UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
4819 ))
4820 .unwrap(),
4821 json!({
4822 "sessionUpdate": "usage_update",
4823 "used": 53000,
4824 "size": 200_000,
4825 "cost": {
4826 "amount": 0.045,
4827 "currency": "USD"
4828 }
4829 })
4830 );
4831
4832 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
4833 "sessionUpdate": "usage_update",
4834 "used": 53000,
4835 "size": 200_000,
4836 "cost": null
4837 }))
4838 .unwrap() else {
4839 panic!("expected usage update");
4840 };
4841
4842 assert_eq!(update.cost, None);
4843 }
4844
4845 #[test]
4846 fn error_stop_reason_carries_failure_beside_discriminator() {
4847 use crate::v2::{Error, ErrorStopReason};
4848 use serde_json::json;
4849
4850 let update =
4851 SessionUpdate::StateUpdate(StateUpdate::Idle(IdleStateUpdate::new().stop_reason(
4852 StopReason::from(ErrorStopReason::new().error(Error::auth_required())),
4853 )));
4854 let wire = json!({
4855 "sessionUpdate": "state_update",
4856 "state": "idle",
4857 "stopReason": "error",
4858 "error": { "code": -32000, "message": "Authentication required" }
4859 });
4860 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4861 assert_eq!(
4862 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4863 update
4864 );
4865
4866 let with_data =
4867 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::from(
4868 ErrorStopReason::new().error(
4869 Error::new(-32099, "Provider unavailable").data(json!({ "status": 503 })),
4870 ),
4871 )));
4872 let wire = json!({
4873 "state": "idle",
4874 "stopReason": "error",
4875 "error": {
4876 "code": -32099,
4877 "message": "Provider unavailable",
4878 "data": { "status": 503 }
4879 }
4880 });
4881 assert_eq!(serde_json::to_value(&with_data).unwrap(), wire);
4882 assert_eq!(
4883 serde_json::from_value::<StateUpdate>(wire).unwrap(),
4884 with_data
4885 );
4886
4887 let without_details = StateUpdate::Idle(
4890 IdleStateUpdate::new().stop_reason(StopReason::from(ErrorStopReason::new())),
4891 );
4892 for error in [
4893 None,
4894 Some(json!(null)),
4895 Some(json!("failed")),
4896 Some(json!({ "message": "missing code" })),
4897 ] {
4898 let mut wire = json!({ "state": "idle", "stopReason": "error" });
4899 if let Some(error) = error {
4900 wire["error"] = error;
4901 }
4902 assert_eq!(
4903 serde_json::from_value::<StateUpdate>(wire).unwrap(),
4904 without_details
4905 );
4906 }
4907 assert_eq!(
4908 serde_json::to_value(&without_details).unwrap(),
4909 json!({ "state": "idle", "stopReason": "error" })
4910 );
4911
4912 let parsed: StateUpdate = serde_json::from_value(json!({
4916 "state": "idle",
4917 "stopReason": "end_turn",
4918 "error": { "code": -32603, "message": "Internal error" }
4919 }))
4920 .unwrap();
4921 assert_eq!(
4922 parsed,
4923 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn))
4924 );
4925 }
4926
4927 #[test]
4928 fn flattened_stop_reason_keeps_open_enum_tolerance() {
4929 use crate::v2::OtherStopReason;
4930 use serde_json::json;
4931
4932 let meta: Meta = [("source".into(), json!("test"))].into_iter().collect();
4933
4934 for stop_reason in [None, Some(json!(null)), Some(json!(true)), Some(json!({}))] {
4937 let mut wire = json!({ "state": "idle", "_meta": { "source": "test" } });
4938 if let Some(stop_reason) = stop_reason {
4939 wire["stopReason"] = stop_reason;
4940 }
4941 let StateUpdate::Idle(idle) = serde_json::from_value(wire).unwrap() else {
4942 panic!("expected idle state update");
4943 };
4944 assert_eq!(idle.stop_reason, None);
4945 assert_eq!(idle.meta, Some(meta.clone()));
4946 }
4947
4948 let wire = json!({
4951 "state": "idle",
4952 "stopReason": "_paused",
4953 "resumeAfter": 30,
4954 "_meta": { "source": "test" }
4955 });
4956 let parsed: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
4957 let expected = StateUpdate::Idle(
4958 IdleStateUpdate::new()
4959 .stop_reason(StopReason::Other(OtherStopReason::new(
4960 "_paused",
4961 [("resumeAfter".into(), json!(30))].into_iter().collect(),
4962 )))
4963 .meta(meta),
4964 );
4965 assert_eq!(parsed, expected);
4966 assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
4967 }
4968
4969 #[test]
4970 fn known_stop_reasons_are_never_read_as_other() {
4971 use crate::v2::{ErrorStopReason, OtherStopReason};
4972 use serde_json::json;
4973
4974 let known = [
4975 StopReason::EndTurn,
4976 StopReason::MaxTokens,
4977 StopReason::MaxTurnRequests,
4978 StopReason::Refusal,
4979 StopReason::Cancelled,
4980 StopReason::Error(ErrorStopReason::new()),
4981 ];
4982 for stop_reason in &known {
4984 match stop_reason {
4985 StopReason::EndTurn
4986 | StopReason::MaxTokens
4987 | StopReason::MaxTurnRequests
4988 | StopReason::Refusal
4989 | StopReason::Cancelled
4990 | StopReason::Error(_)
4991 | StopReason::Other(_) => {}
4992 }
4993 }
4994
4995 for stop_reason in known {
4996 let update = StateUpdate::Idle(IdleStateUpdate::new().stop_reason(stop_reason));
4997 let wire = serde_json::to_value(&update).unwrap();
4998 assert_eq!(
4999 serde_json::from_value::<StateUpdate>(wire.clone()).unwrap(),
5000 update
5001 );
5002 assert!(
5003 serde_json::from_value::<OtherStopReason>(
5004 json!({ "stopReason": wire["stopReason"] })
5005 )
5006 .is_err(),
5007 "{} must not be accepted as a custom stop reason",
5008 wire["stopReason"]
5009 );
5010 }
5011 }
5012
5013 #[test]
5014 fn unknown_stop_reason_leaves_usage_on_the_idle_update() {
5015 use serde_json::json;
5016
5017 let usage = json!({ "totalTokens": 10, "inputTokens": 6, "outputTokens": 4 });
5018 let wire = json!({ "state": "idle", "stopReason": "_paused", "usage": usage });
5019 let StateUpdate::Idle(idle) = serde_json::from_value(wire.clone()).unwrap() else {
5020 panic!("expected idle state update");
5021 };
5022 let Some(StopReason::Other(other)) = &idle.stop_reason else {
5023 panic!("expected a custom stop reason");
5024 };
5025 assert!(other.fields.is_empty());
5026
5027 #[cfg(feature = "unstable_end_turn_token_usage")]
5028 {
5029 assert!(idle.usage.is_some());
5030 assert_eq!(serde_json::to_value(&idle).unwrap()["usage"], usage);
5031 }
5032 #[cfg(not(feature = "unstable_end_turn_token_usage"))]
5035 assert!(serde_json::to_value(&idle).unwrap().get("usage").is_none());
5036 }
5037
5038 #[test]
5039 fn test_state_update_serialization() {
5040 use serde_json::json;
5041
5042 assert_eq!(
5043 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
5044 RunningStateUpdate::new()
5045 )))
5046 .unwrap(),
5047 json!({
5048 "sessionUpdate": "state_update",
5049 "state": "running"
5050 })
5051 );
5052
5053 assert_eq!(
5054 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
5055 IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
5056 )))
5057 .unwrap(),
5058 json!({
5059 "sessionUpdate": "state_update",
5060 "state": "idle",
5061 "stopReason": "end_turn"
5062 })
5063 );
5064
5065 let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
5066 "sessionUpdate": "state_update",
5067 "state": "requires_action"
5068 }))
5069 .unwrap() else {
5070 panic!("expected state update");
5071 };
5072
5073 assert!(matches!(update, StateUpdate::RequiresAction(_)));
5074
5075 let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
5076 "sessionUpdate": "state_update",
5077 "state": "idle",
5078 "stopReason": null
5079 }))
5080 .unwrap() else {
5081 panic!("expected idle state update");
5082 };
5083
5084 assert_eq!(update.stop_reason, None);
5085
5086 let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
5087 serde_json::from_value(json!({
5088 "sessionUpdate": "state_update",
5089 "state": "_paused",
5090 "label": "Paused"
5091 }))
5092 .unwrap()
5093 else {
5094 panic!("expected unknown state update");
5095 };
5096
5097 assert_eq!(update.state, "_paused");
5098 assert_eq!(update.fields["label"], json!("Paused"));
5099 }
5100
5101 #[test]
5102 fn session_update_preserves_unknown_variant() {
5103 use serde_json::json;
5104
5105 let update: SessionUpdate = serde_json::from_value(json!({
5106 "sessionUpdate": "_status_badge",
5107 "label": "Indexing",
5108 "progress": 0.5
5109 }))
5110 .unwrap();
5111
5112 let SessionUpdate::Other(unknown) = update else {
5113 panic!("expected unknown session update");
5114 };
5115
5116 assert_eq!(unknown.session_update, "_status_badge");
5117 assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
5118 assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
5119
5120 assert_eq!(
5121 serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
5122 json!({
5123 "sessionUpdate": "_status_badge",
5124 "label": "Indexing",
5125 "progress": 0.5
5126 })
5127 );
5128 }
5129
5130 #[test]
5131 fn terminal_session_updates_use_known_discriminators() {
5132 use serde_json::json;
5133
5134 assert_eq!(
5135 serde_json::to_value(SessionUpdate::TerminalUpdate(
5136 TerminalUpdate::new("term_1").command("cargo test")
5137 ))
5138 .unwrap(),
5139 json!({
5140 "sessionUpdate": "terminal_update",
5141 "terminalId": "term_1",
5142 "command": "cargo test"
5143 })
5144 );
5145 assert_eq!(
5146 serde_json::to_value(SessionUpdate::TerminalOutputChunk(
5147 TerminalOutputChunk::new("term_1", "dGVzdAo=")
5148 ))
5149 .unwrap(),
5150 json!({
5151 "sessionUpdate": "terminal_output_chunk",
5152 "terminalId": "term_1",
5153 "data": "dGVzdAo="
5154 })
5155 );
5156 }
5157
5158 #[test]
5159 fn session_update_does_not_hide_malformed_known_terminal_variants() {
5160 use serde_json::json;
5161
5162 assert!(
5163 serde_json::from_value::<SessionUpdate>(json!({
5164 "sessionUpdate": "terminal_update"
5165 }))
5166 .is_err()
5167 );
5168 assert!(
5169 serde_json::from_value::<SessionUpdate>(json!({
5170 "sessionUpdate": "terminal_output_chunk",
5171 "terminalId": "term_1"
5172 }))
5173 .is_err()
5174 );
5175 }
5176
5177 #[test]
5178 fn test_plan_update_serialization() {
5179 use serde_json::json;
5180
5181 let plan_update =
5182 SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
5183 "plan-1",
5184 vec![crate::v2::PlanEntry::new(
5185 "Step 1",
5186 crate::v2::PlanEntryPriority::High,
5187 crate::v2::PlanEntryStatus::Pending,
5188 )],
5189 )));
5190
5191 assert_eq!(
5192 serde_json::to_value(plan_update).unwrap(),
5193 json!({
5194 "sessionUpdate": "plan_update",
5195 "plan": {
5196 "type": "items",
5197 "planId": "plan-1",
5198 "entries": [
5199 {
5200 "content": "Step 1",
5201 "priority": "high",
5202 "status": "pending"
5203 }
5204 ]
5205 }
5206 })
5207 );
5208 }
5209
5210 #[cfg(feature = "unstable_plan_operations")]
5211 #[test]
5212 fn test_plan_removed_serialization() {
5213 use serde_json::json;
5214
5215 assert_eq!(
5216 serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
5217 json!({
5218 "sessionUpdate": "plan_removed",
5219 "planId": "plan-1"
5220 })
5221 );
5222 }
5223
5224 #[test]
5225 fn available_command_uses_type_discriminator() {
5226 use serde_json::json;
5227
5228 let command = AvailableCommand::Command(
5229 Command::new("web", "Search the web")
5230 .input(AvailableCommandInput::Text(TextCommandInput::new("Query"))),
5231 );
5232
5233 let json = serde_json::to_value(&command).unwrap();
5234 assert_eq!(
5235 json,
5236 json!({
5237 "type": "command",
5238 "name": "web",
5239 "description": "Search the web",
5240 "input": { "type": "text", "hint": "Query" }
5241 })
5242 );
5243
5244 let roundtripped: AvailableCommand = serde_json::from_value(json).unwrap();
5245 assert_eq!(roundtripped, command);
5246 assert_eq!(roundtripped.name(), "web");
5247 assert_eq!(roundtripped.description(), "Search the web");
5248 }
5249
5250 #[test]
5251 fn available_command_preserves_unknown_typed_variant() {
5252 use serde_json::json;
5253
5254 let payload = json!({
5255 "type": "_skill",
5256 "name": "pdf-processing",
5257 "description": "Extract text from PDFs",
5258 "uri": "skill://pdf-processing/SKILL.md",
5259 "_meta": { "example.com/source": "mcp" }
5260 });
5261 let command: AvailableCommand = serde_json::from_value(payload.clone()).unwrap();
5262
5263 assert_eq!(command.name(), "pdf-processing");
5264 assert_eq!(command.description(), "Extract text from PDFs");
5265 assert!(command.meta().is_some());
5266
5267 let AvailableCommand::Other(unknown) = &command else {
5268 panic!("expected unknown command");
5269 };
5270 assert_eq!(unknown.type_, "_skill");
5271 assert_eq!(
5272 unknown.fields.get("uri"),
5273 Some(&json!("skill://pdf-processing/SKILL.md"))
5274 );
5275 assert_eq!(serde_json::to_value(&command).unwrap(), payload);
5276 }
5277
5278 #[test]
5279 fn available_command_unknown_tolerates_invalid_meta() {
5280 use serde_json::json;
5281
5282 let command: AvailableCommand = serde_json::from_value(json!({
5283 "type": "_skill",
5284 "name": "pdf-processing",
5285 "description": "Extract text from PDFs",
5286 "_meta": "oops"
5287 }))
5288 .unwrap();
5289
5290 assert!(matches!(command, AvailableCommand::Other(_)));
5291 assert!(command.meta().is_none());
5292 }
5293
5294 #[test]
5295 fn available_command_unknown_does_not_hide_malformed_command_variant() {
5296 use serde_json::json;
5297
5298 assert!(
5300 serde_json::from_value::<AvailableCommand>(json!({
5301 "name": "test",
5302 "description": "Run project tests"
5303 }))
5304 .is_err()
5305 );
5306 assert!(
5307 serde_json::from_value::<AvailableCommand>(json!({
5308 "type": "command",
5309 "name": "test"
5310 }))
5311 .is_err()
5312 );
5313 assert!(
5314 serde_json::from_value::<OtherAvailableCommand>(json!({
5315 "type": "command",
5316 "name": "test",
5317 "description": "Run project tests"
5318 }))
5319 .is_err()
5320 );
5321 assert!(
5322 serde_json::from_value::<AvailableCommand>(json!({
5323 "type": "_skill",
5324 "name": "pdf-processing"
5325 }))
5326 .is_err()
5327 );
5328 }
5329
5330 #[test]
5331 fn available_command_input_preserves_unknown_typed_variant() {
5332 use serde_json::json;
5333
5334 let input: AvailableCommandInput = serde_json::from_value(json!({
5335 "type": "_choices",
5336 "hint": "Pick one",
5337 "options": ["fast", "careful"]
5338 }))
5339 .unwrap();
5340
5341 let AvailableCommandInput::Other(unknown) = input else {
5342 panic!("expected unknown command input");
5343 };
5344
5345 assert_eq!(unknown.type_, "_choices");
5346 assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
5347 assert_eq!(
5348 unknown.fields.get("options"),
5349 Some(&json!(["fast", "careful"]))
5350 );
5351 assert_eq!(
5352 serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
5353 json!({
5354 "type": "_choices",
5355 "hint": "Pick one",
5356 "options": ["fast", "careful"]
5357 })
5358 );
5359 }
5360
5361 #[test]
5362 fn available_command_input_text_uses_type_discriminator() {
5363 use serde_json::json;
5364
5365 let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
5366
5367 let json = serde_json::to_value(&input).unwrap();
5368 assert_eq!(
5369 json,
5370 json!({
5371 "type": "text",
5372 "hint": "Describe changes"
5373 })
5374 );
5375
5376 let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
5377 assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
5378 }
5379
5380 #[test]
5381 fn request_permission_subject_tool_call_uses_type_discriminator() {
5382 use serde_json::json;
5383
5384 let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
5385
5386 let json = serde_json::to_value(&subject).unwrap();
5387 assert_eq!(
5388 json,
5389 json!({
5390 "type": "tool_call",
5391 "toolCall": {
5392 "toolCallId": "call_001"
5393 }
5394 })
5395 );
5396
5397 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5398 assert!(matches!(
5399 roundtripped,
5400 RequestPermissionSubject::ToolCall(_)
5401 ));
5402 }
5403
5404 #[test]
5405 fn request_permission_subject_command_uses_type_discriminator() {
5406 use serde_json::json;
5407
5408 let mut meta = Meta::new();
5409 meta.insert("source".to_string(), json!("shell"));
5410 let subject = RequestPermissionSubject::from(
5411 CommandPermissionSubject::new("cargo test", "/workspace/project")
5412 .tool_call_id("call_001")
5413 .terminal_id("term_1")
5414 .meta(meta),
5415 );
5416
5417 let json = serde_json::to_value(&subject).unwrap();
5418 assert_eq!(
5419 json,
5420 json!({
5421 "type": "command",
5422 "command": "cargo test",
5423 "cwd": "/workspace/project",
5424 "toolCallId": "call_001",
5425 "terminalId": "term_1",
5426 "_meta": {
5427 "source": "shell"
5428 }
5429 })
5430 );
5431
5432 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5433 assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
5434 }
5435
5436 #[test]
5437 fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
5438 use serde_json::json;
5439
5440 let subject: RequestPermissionSubject = serde_json::from_value(json!({
5441 "type": "command",
5442 "command": "cargo test",
5443 "cwd": "/workspace/project",
5444 "toolCallId": null,
5445 "terminalId": null,
5446 "_meta": null
5447 }))
5448 .unwrap();
5449
5450 let RequestPermissionSubject::Command(subject) = subject else {
5451 panic!("expected command permission subject");
5452 };
5453 assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
5454 assert_eq!(subject.tool_call_id, None);
5455 assert_eq!(subject.terminal_id, None);
5456 assert_eq!(subject.meta, None);
5457 assert_eq!(
5458 serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
5459 json!({
5460 "type": "command",
5461 "command": "cargo test",
5462 "cwd": "/workspace/project"
5463 })
5464 );
5465 }
5466
5467 #[test]
5468 fn request_permission_subject_preserves_unknown_variant() {
5469 use serde_json::json;
5470
5471 let subject: RequestPermissionSubject = serde_json::from_value(json!({
5472 "type": "_review",
5473 "reason": "needs-review",
5474 "retryAfterSeconds": 30
5475 }))
5476 .unwrap();
5477
5478 let RequestPermissionSubject::Other(unknown) = subject else {
5479 panic!("expected unknown permission subject");
5480 };
5481
5482 assert_eq!(unknown.type_, "_review");
5483 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
5484 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
5485 assert_eq!(
5486 serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
5487 json!({
5488 "type": "_review",
5489 "reason": "needs-review",
5490 "retryAfterSeconds": 30
5491 })
5492 );
5493 }
5494
5495 #[test]
5496 fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
5497 use serde_json::json;
5498
5499 assert!(
5500 serde_json::from_value::<RequestPermissionSubject>(json!({
5501 "type": "tool_call"
5502 }))
5503 .is_err()
5504 );
5505 assert!(
5506 serde_json::from_value::<RequestPermissionSubject>(json!({
5507 "type": 1
5508 }))
5509 .is_err()
5510 );
5511 assert!(
5512 serde_json::from_value::<RequestPermissionSubject>(json!({
5513 "type": "command",
5514 "cwd": "/workspace/project"
5515 }))
5516 .is_err()
5517 );
5518 assert!(
5519 serde_json::from_value::<RequestPermissionSubject>(json!({
5520 "type": "command",
5521 "command": "cargo test"
5522 }))
5523 .is_err()
5524 );
5525 assert!(
5526 serde_json::from_value::<RequestPermissionSubject>(json!({
5527 "type": "command",
5528 "command": "cargo test",
5529 "cwd": null
5530 }))
5531 .is_err()
5532 );
5533 }
5534
5535 #[test]
5536 fn request_permission_title_and_description_are_separate_from_tool_call_content() {
5537 use serde_json::json;
5538
5539 let request =
5540 RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
5541 .description("Allow this tool to edit src/main.rs?")
5542 .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
5543 "call_001",
5544 )));
5545
5546 assert_eq!(
5547 serde_json::to_value(request).unwrap(),
5548 json!({
5549 "sessionId": "sess_abc123def456",
5550 "title": "Approve file edit?",
5551 "description": "Allow this tool to edit src/main.rs?",
5552 "subject": {
5553 "type": "tool_call",
5554 "toolCall": {
5555 "toolCallId": "call_001"
5556 }
5557 },
5558 "options": []
5559 })
5560 );
5561 }
5562
5563 #[test]
5564 fn request_permission_requires_title_and_allows_missing_subject() {
5565 use serde_json::json;
5566
5567 let request = RequestPermissionRequest::new(
5568 "sess_abc123def456",
5569 "Approve elevated permissions?",
5570 Vec::new(),
5571 );
5572
5573 assert_eq!(
5574 serde_json::to_value(request).unwrap(),
5575 json!({
5576 "sessionId": "sess_abc123def456",
5577 "title": "Approve elevated permissions?",
5578 "options": []
5579 })
5580 );
5581
5582 let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
5583 "sessionId": "sess_abc123def456",
5584 "title": "Approve elevated permissions?",
5585 "options": []
5586 }))
5587 .unwrap();
5588 assert!(missing_subject.subject.is_none());
5589
5590 let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
5591 "sessionId": "sess_abc123def456",
5592 "title": "Approve elevated permissions?",
5593 "subject": null,
5594 "options": []
5595 }))
5596 .unwrap();
5597 assert!(null_subject.subject.is_none());
5598
5599 assert!(
5600 serde_json::from_value::<RequestPermissionRequest>(json!({
5601 "sessionId": "sess_abc123def456",
5602 "options": []
5603 }))
5604 .is_err()
5605 );
5606 }
5607
5608 #[test]
5609 fn request_permission_outcome_preserves_unknown_variant() {
5610 use serde_json::json;
5611
5612 let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
5613 "outcome": "_defer",
5614 "reason": "needs-review",
5615 "retryAfterSeconds": 30
5616 }))
5617 .unwrap();
5618
5619 let RequestPermissionOutcome::Other(unknown) = outcome else {
5620 panic!("expected unknown permission outcome");
5621 };
5622
5623 assert_eq!(unknown.outcome, "_defer");
5624 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
5625 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
5626 assert_eq!(
5627 serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
5628 json!({
5629 "outcome": "_defer",
5630 "reason": "needs-review",
5631 "retryAfterSeconds": 30
5632 })
5633 );
5634 }
5635
5636 #[test]
5637 fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
5638 use serde_json::json;
5639
5640 assert!(
5641 serde_json::from_value::<RequestPermissionOutcome>(json!({
5642 "outcome": "selected"
5643 }))
5644 .is_err()
5645 );
5646 assert!(
5647 serde_json::from_value::<RequestPermissionOutcome>(json!({
5648 "outcome": 1
5649 }))
5650 .is_err()
5651 );
5652 }
5653
5654 #[test]
5655 fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
5656 use serde_json::json;
5657
5658 assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
5659 assert!(
5660 serde_json::from_value::<AvailableCommandInput>(json!({
5661 "hint": "Pick one"
5662 }))
5663 .is_err()
5664 );
5665 assert!(
5666 serde_json::from_value::<AvailableCommandInput>(json!({
5667 "type": 1,
5668 "hint": "Pick one"
5669 }))
5670 .is_err()
5671 );
5672 assert!(
5673 serde_json::from_value::<OtherAvailableCommandInput>(json!({
5674 "type": "text",
5675 "hint": "Pick one"
5676 }))
5677 .is_err()
5678 );
5679 }
5680
5681 #[cfg(feature = "unstable_mcp_over_acp")]
5682 #[test]
5683 fn test_agent_mcp_request_method_names() {
5684 use serde_json::json;
5685
5686 let params: serde_json::Map<String, serde_json::Value> =
5687 [("cursor".to_string(), json!("abc"))].into_iter().collect();
5688
5689 assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
5690 assert_eq!(
5691 AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
5692 "server-1",
5693 "req-1",
5694 "tools/list"
5695 )))
5696 .method(),
5697 "mcp/message"
5698 );
5699 assert_eq!(
5700 serde_json::to_value(
5701 MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
5702 )
5703 .unwrap(),
5704 json!({
5705 "serverId": "server-1",
5706 "requestId": "req-1",
5707 "method": "tools/list",
5708 "params": { "cursor": "abc" }
5709 })
5710 );
5711
5712 let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
5713 "serverId": "server-1",
5714 "requestId": "req-1",
5715 "method": "tools/list",
5716 "params": null,
5717 "_meta": null
5718 }))
5719 .unwrap();
5720 assert_eq!(request_with_null_params.params, None);
5721 assert_eq!(request_with_null_params.meta, None);
5722 for key in ["serverId", "requestId", "method"] {
5723 let mut value =
5724 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5725 value.as_object_mut().unwrap().remove(key);
5726 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5727 }
5728 for key in ["serverId", "requestId", "method"] {
5729 let mut value =
5730 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5731 value[key] = serde_json::Value::Null;
5732 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5733 }
5734 }
5735
5736 #[test]
5737 fn test_auth_capabilities_serialize_terminal_support_as_object() {
5738 use serde_json::json;
5739
5740 let capabilities = ClientAuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
5741
5742 assert_eq!(
5743 serde_json::to_value(&capabilities).unwrap(),
5744 json!({
5745 "terminal": {}
5746 })
5747 );
5748
5749 let deserialized: ClientAuthCapabilities = serde_json::from_value(json!({
5750 "terminal": false
5751 }))
5752 .unwrap();
5753 assert!(deserialized.terminal.is_none());
5754 }
5755
5756 #[test]
5757 fn request_permission_request_rejects_malformed_options() {
5758 use serde_json::json;
5759
5760 assert!(
5761 serde_json::from_value::<RequestPermissionRequest>(json!({
5762 "sessionId": "sess-1",
5763 "title": "Run tool?",
5764 "options": "not-an-array"
5765 }))
5766 .is_err()
5767 );
5768 assert!(
5769 serde_json::from_value::<RequestPermissionRequest>(json!({
5770 "sessionId": "sess-1",
5771 "title": "Run tool?",
5772 "options": [{"optionId": "allow"}]
5773 }))
5774 .is_err()
5775 );
5776 }
5777
5778 #[cfg(feature = "unstable_plan_operations")]
5779 #[test]
5780 fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
5781 use serde_json::json;
5782
5783 assert!(
5784 serde_json::from_value::<SessionUpdate>(json!({
5785 "sessionUpdate": "plan_removed"
5786 }))
5787 .is_err()
5788 );
5789 }
5790}