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")]
967#[serde_as]
968#[skip_serializing_none]
969#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
970#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
971#[serde(rename_all = "camelCase")]
972#[non_exhaustive]
973pub struct SubagentUpdate {
974 pub session_id: SessionId,
976 #[serde_as(deserialize_as = "DefaultOnError")]
981 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
982 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
983 pub title: MaybeUndefined<String>,
984 #[serde_as(deserialize_as = "DefaultOnError")]
989 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
990 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
991 pub description: MaybeUndefined<String>,
992 #[serde_as(deserialize_as = "DefaultOnError")]
997 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
998 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
999 pub capabilities: MaybeUndefined<SubagentSessionCapabilities>,
1000 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1006 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1007 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1008 pub state: MaybeUndefined<StateUpdate>,
1009 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1015 pub cwd: MaybeUndefined<AbsolutePath>,
1016 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1023 pub additional_directories: MaybeUndefined<Vec<AbsolutePath>>,
1024 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1033 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1034 #[serde(
1035 rename = "_meta",
1036 default,
1037 skip_serializing_if = "MaybeUndefined::is_undefined"
1038 )]
1039 pub meta: MaybeUndefined<Meta>,
1040}
1041
1042#[cfg(feature = "unstable_subagents")]
1043impl SubagentUpdate {
1044 #[must_use]
1046 pub fn new(session_id: impl Into<SessionId>) -> Self {
1047 Self {
1048 session_id: session_id.into(),
1049 title: MaybeUndefined::Undefined,
1050 description: MaybeUndefined::Undefined,
1051 capabilities: MaybeUndefined::Undefined,
1052 state: MaybeUndefined::Undefined,
1053 cwd: MaybeUndefined::Undefined,
1054 additional_directories: MaybeUndefined::Undefined,
1055 meta: MaybeUndefined::Undefined,
1056 }
1057 }
1058
1059 #[must_use]
1061 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1062 self.title = title.into_maybe_undefined();
1063 self
1064 }
1065
1066 #[must_use]
1068 pub fn description(mut self, description: impl IntoMaybeUndefined<String>) -> Self {
1069 self.description = description.into_maybe_undefined();
1070 self
1071 }
1072
1073 #[must_use]
1075 pub fn capabilities(
1076 mut self,
1077 capabilities: impl IntoMaybeUndefined<SubagentSessionCapabilities>,
1078 ) -> Self {
1079 self.capabilities = capabilities.into_maybe_undefined();
1080 self
1081 }
1082
1083 #[must_use]
1085 pub fn state(mut self, state: impl IntoMaybeUndefined<StateUpdate>) -> Self {
1086 self.state = state.into_maybe_undefined();
1087 self
1088 }
1089
1090 #[must_use]
1094 pub fn cwd(mut self, cwd: impl IntoMaybeUndefined<AbsolutePath>) -> Self {
1095 self.cwd = cwd.into_maybe_undefined();
1096 self
1097 }
1098
1099 #[must_use]
1103 pub fn additional_directories(
1104 mut self,
1105 additional_directories: impl IntoMaybeUndefined<Vec<AbsolutePath>>,
1106 ) -> Self {
1107 self.additional_directories = additional_directories.into_maybe_undefined();
1108 self
1109 }
1110
1111 #[must_use]
1116 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1117 self.meta = meta.into_maybe_undefined();
1118 self
1119 }
1120}
1121
1122#[cfg(feature = "unstable_subagents")]
1131#[serde_as]
1132#[skip_serializing_none]
1133#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1134#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1135#[serde(rename_all = "camelCase")]
1136#[non_exhaustive]
1137pub struct SubagentSessionCapabilities {
1138 #[serde_as(deserialize_as = "DefaultOnError")]
1142 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1143 #[serde(default)]
1144 pub cancel: Option<SessionCancelCapabilities>,
1145 #[serde_as(deserialize_as = "DefaultOnError")]
1151 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1152 #[serde(default)]
1153 #[serde(rename = "_meta")]
1154 pub meta: Option<Meta>,
1155}
1156
1157#[cfg(feature = "unstable_subagents")]
1158impl SubagentSessionCapabilities {
1159 #[must_use]
1161 pub fn new() -> Self {
1162 Self::default()
1163 }
1164
1165 #[must_use]
1167 pub fn cancel(mut self, cancel: impl IntoOption<SessionCancelCapabilities>) -> Self {
1168 self.cancel = cancel.into_option();
1169 self
1170 }
1171
1172 #[must_use]
1176 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1177 self.meta = meta.into_option();
1178 self
1179 }
1180}
1181
1182#[cfg(feature = "unstable_subagents")]
1190#[serde_as]
1191#[skip_serializing_none]
1192#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1193#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1194#[non_exhaustive]
1195pub struct SessionCancelCapabilities {
1196 #[serde_as(deserialize_as = "DefaultOnError")]
1202 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1203 #[serde(default)]
1204 #[serde(rename = "_meta")]
1205 pub meta: Option<Meta>,
1206}
1207
1208#[cfg(feature = "unstable_subagents")]
1209impl SessionCancelCapabilities {
1210 #[must_use]
1212 pub fn new() -> Self {
1213 Self::default()
1214 }
1215
1216 #[must_use]
1222 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1223 self.meta = meta.into_option();
1224 self
1225 }
1226}
1227
1228#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1234#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1235#[cfg_attr(feature = "schemars", schemars(inline))]
1236#[cfg_attr(feature = "schemars", schemars(transform = other_session_update_schema))]
1237#[serde(rename_all = "camelCase")]
1238#[non_exhaustive]
1239pub struct OtherSessionUpdate {
1240 #[serde(rename = "sessionUpdate")]
1246 pub session_update: String,
1247 #[serde(flatten)]
1249 pub fields: BTreeMap<String, serde_json::Value>,
1250}
1251
1252impl OtherSessionUpdate {
1253 #[must_use]
1255 pub fn new(
1256 session_update: impl Into<String>,
1257 mut fields: BTreeMap<String, serde_json::Value>,
1258 ) -> Self {
1259 fields.remove("sessionUpdate");
1260 Self {
1261 session_update: session_update.into(),
1262 fields,
1263 }
1264 }
1265}
1266
1267impl<'de> Deserialize<'de> for OtherSessionUpdate {
1268 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1269 where
1270 D: serde::Deserializer<'de>,
1271 {
1272 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1273 let session_update = fields
1274 .remove("sessionUpdate")
1275 .ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
1276 let serde_json::Value::String(session_update) = session_update else {
1277 return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
1278 };
1279
1280 if is_known_session_update(&session_update) {
1281 return Err(serde::de::Error::custom(format!(
1282 "known session update `{session_update}` did not match its schema"
1283 )));
1284 }
1285
1286 Ok(Self {
1287 session_update,
1288 fields,
1289 })
1290 }
1291}
1292
1293fn is_known_session_update(session_update: &str) -> bool {
1294 if session_update == "notice" {
1295 return true;
1296 }
1297 if matches!(
1298 session_update,
1299 "compaction_update" | "compaction_summary_chunk"
1300 ) {
1301 return true;
1302 }
1303 #[cfg(feature = "unstable_plan_operations")]
1304 if session_update == "plan_removed" {
1305 return true;
1306 }
1307 #[cfg(feature = "unstable_subagents")]
1308 if matches!(
1309 session_update,
1310 "subagent_update" | "session_message" | "session_message_chunk"
1311 ) {
1312 return true;
1313 }
1314 matches!(
1315 session_update,
1316 "user_message_chunk"
1317 | "user_message"
1318 | "agent_message_chunk"
1319 | "agent_message"
1320 | "agent_thought_chunk"
1321 | "agent_thought"
1322 | "state_update"
1323 | "tool_call_content_chunk"
1324 | "tool_call_update"
1325 | "terminal_update"
1326 | "terminal_output_chunk"
1327 | "plan_update"
1328 | "available_commands_update"
1329 | "config_option_update"
1330 | "session_info_update"
1331 | "usage_update"
1332 )
1333}
1334
1335#[cfg(feature = "schemars")]
1336fn other_session_update_schema(schema: &mut Schema) {
1337 super::schema_util::reject_known_string_discriminators(
1338 schema,
1339 "sessionUpdate",
1340 &[
1341 "user_message_chunk",
1342 "user_message",
1343 "agent_message_chunk",
1344 "agent_message",
1345 "agent_thought_chunk",
1346 "agent_thought",
1347 "state_update",
1348 "tool_call_content_chunk",
1349 "tool_call_update",
1350 "terminal_update",
1351 "terminal_output_chunk",
1352 "plan_update",
1353 "available_commands_update",
1354 "config_option_update",
1355 "session_info_update",
1356 #[cfg(feature = "unstable_plan_operations")]
1357 "plan_removed",
1358 "usage_update",
1359 "notice",
1360 "compaction_update",
1361 "compaction_summary_chunk",
1362 #[cfg(feature = "unstable_subagents")]
1363 "subagent_update",
1364 #[cfg(feature = "unstable_subagents")]
1365 "session_message",
1366 #[cfg(feature = "unstable_subagents")]
1367 "session_message_chunk",
1368 ],
1369 );
1370}
1371
1372#[serde_as]
1374#[skip_serializing_none]
1375#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1377#[serde(rename_all = "camelCase")]
1378#[non_exhaustive]
1379pub struct ConfigOptionUpdate {
1380 #[serde_as(deserialize_as = "VecSkipError<_>")]
1382 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
1383 pub config_options: Vec<SessionConfigOption>,
1384 #[serde_as(deserialize_as = "DefaultOnError")]
1390 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1391 #[serde(default)]
1392 #[serde(rename = "_meta")]
1393 pub meta: Option<Meta>,
1394}
1395
1396impl ConfigOptionUpdate {
1397 #[must_use]
1399 pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
1400 Self {
1401 config_options,
1402 meta: None,
1403 }
1404 }
1405
1406 #[must_use]
1412 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1413 self.meta = meta.into_option();
1414 self
1415 }
1416}
1417
1418#[serde_as]
1427#[skip_serializing_none]
1428#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1429#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1430#[serde(rename_all = "camelCase")]
1431#[non_exhaustive]
1432pub struct SessionInfoUpdate {
1433 #[serde_as(deserialize_as = "DefaultOnError")]
1435 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1436 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1437 pub title: MaybeUndefined<String>,
1438 #[serde_as(deserialize_as = "DefaultOnError")]
1440 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
1441 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1442 pub updated_at: MaybeUndefined<String>,
1443 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1452 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1453 #[serde(
1454 rename = "_meta",
1455 default,
1456 skip_serializing_if = "MaybeUndefined::is_undefined"
1457 )]
1458 pub meta: MaybeUndefined<Meta>,
1459}
1460
1461impl SessionInfoUpdate {
1462 #[must_use]
1464 pub fn new() -> Self {
1465 Self::default()
1466 }
1467
1468 #[must_use]
1470 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1471 self.title = title.into_maybe_undefined();
1472 self
1473 }
1474
1475 #[must_use]
1477 pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
1478 self.updated_at = updated_at.into_maybe_undefined();
1479 self
1480 }
1481
1482 #[must_use]
1487 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1488 self.meta = meta.into_maybe_undefined();
1489 self
1490 }
1491}
1492
1493#[serde_as]
1495#[skip_serializing_none]
1496#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1498#[serde(rename_all = "camelCase")]
1499#[non_exhaustive]
1500pub struct UsageUpdate {
1501 pub used: u64,
1503 pub size: u64,
1505 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1509 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1510 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1511 pub cost: MaybeUndefined<Cost>,
1512 #[serde_as(deserialize_as = "DefaultOnError")]
1518 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1519 #[serde(default)]
1520 #[serde(rename = "_meta")]
1521 pub meta: Option<Meta>,
1522}
1523
1524impl UsageUpdate {
1525 #[must_use]
1527 pub fn new(used: u64, size: u64) -> Self {
1528 Self {
1529 used,
1530 size,
1531 cost: MaybeUndefined::Undefined,
1532 meta: None,
1533 }
1534 }
1535
1536 #[must_use]
1538 pub fn cost(mut self, cost: impl IntoMaybeUndefined<Cost>) -> Self {
1539 self.cost = cost.into_maybe_undefined();
1540 self
1541 }
1542
1543 #[must_use]
1549 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1550 self.meta = meta.into_option();
1551 self
1552 }
1553}
1554
1555#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1596#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1597#[serde(tag = "state", rename_all = "snake_case")]
1598#[non_exhaustive]
1599pub enum StateUpdate {
1600 Running(RunningStateUpdate),
1602 Idle(IdleStateUpdate),
1604 RequiresAction(RequiresActionStateUpdate),
1606 #[cfg(feature = "unstable_subagents")]
1613 Unknown(UnknownStateUpdate),
1614 #[serde(untagged)]
1626 Other(OtherStateUpdate),
1627}
1628
1629#[serde_as]
1631#[skip_serializing_none]
1632#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1633#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1634#[serde(rename_all = "camelCase")]
1635#[non_exhaustive]
1636pub struct RunningStateUpdate {
1637 #[serde_as(deserialize_as = "DefaultOnError")]
1643 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1644 #[serde(default)]
1645 #[serde(rename = "_meta")]
1646 pub meta: Option<Meta>,
1647}
1648
1649impl RunningStateUpdate {
1650 #[must_use]
1652 pub fn new() -> Self {
1653 Self::default()
1654 }
1655
1656 #[must_use]
1662 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1663 self.meta = meta.into_option();
1664 self
1665 }
1666}
1667
1668#[serde_as]
1677#[skip_serializing_none]
1678#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1679#[cfg_attr(feature = "schemars", schemars(transform = idle_state_update_schema))]
1680#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1681#[serde(rename_all = "camelCase")]
1682#[non_exhaustive]
1683pub struct IdleStateUpdate {
1684 #[serde(flatten)]
1686 pub stop_reason: Option<StopReason>,
1687 #[cfg(feature = "unstable_end_turn_token_usage")]
1696 #[serde_as(deserialize_as = "DefaultOnError")]
1697 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1698 #[serde(default)]
1699 pub usage: Option<Box<Usage>>,
1700 #[serde_as(deserialize_as = "DefaultOnError")]
1706 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1707 #[serde(default)]
1708 #[serde(rename = "_meta")]
1709 pub meta: Option<Meta>,
1710}
1711
1712impl IdleStateUpdate {
1713 #[must_use]
1715 pub fn new() -> Self {
1716 Self::default()
1717 }
1718
1719 #[must_use]
1721 pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
1722 self.stop_reason = stop_reason.into_option();
1723 self
1724 }
1725
1726 #[cfg(feature = "unstable_end_turn_token_usage")]
1732 #[must_use]
1733 pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
1734 self.usage = usage.into_option().map(Box::new);
1735 self
1736 }
1737
1738 #[must_use]
1744 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1745 self.meta = meta.into_option();
1746 self
1747 }
1748}
1749
1750#[cfg(feature = "schemars")]
1759fn idle_state_update_schema(schema: &mut Schema) {
1760 if let Some(properties) = schema
1761 .get_mut("properties")
1762 .and_then(serde_json::Value::as_object_mut)
1763 {
1764 properties.insert(
1765 "stopReason".into(),
1766 serde_json::json!({
1767 "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)",
1768 "type": ["string", "null"],
1769 "x-deserialize-default-on-error": true
1770 }),
1771 );
1772 }
1773 let Some(variants) = schema
1774 .get_mut("anyOf")
1775 .and_then(serde_json::Value::as_array_mut)
1776 else {
1777 return;
1778 };
1779 for variant in variants {
1780 if variant.as_object().is_some_and(serde_json::Map::is_empty) {
1781 *variant = serde_json::json!({
1782 "title": "none",
1783 "description": "No stop reason: `stopReason` is omitted or `null`.",
1784 "type": "object",
1785 "properties": {
1786 "stopReason": { "type": "null" }
1787 }
1788 });
1789 }
1790 }
1791}
1792
1793#[serde_as]
1802#[skip_serializing_none]
1803#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1804#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1805#[serde(rename_all = "camelCase")]
1806#[non_exhaustive]
1807pub struct RequiresActionStateUpdate {
1808 #[serde_as(deserialize_as = "DefaultOnError")]
1814 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1815 #[serde(default)]
1816 #[serde(rename = "_meta")]
1817 pub meta: Option<Meta>,
1818}
1819
1820impl RequiresActionStateUpdate {
1821 #[must_use]
1823 pub fn new() -> Self {
1824 Self::default()
1825 }
1826
1827 #[must_use]
1833 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1834 self.meta = meta.into_option();
1835 self
1836 }
1837}
1838
1839#[cfg(feature = "unstable_subagents")]
1853#[serde_as]
1854#[skip_serializing_none]
1855#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1856#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1857#[serde(rename_all = "camelCase")]
1858#[non_exhaustive]
1859pub struct UnknownStateUpdate {
1860 #[serde_as(deserialize_as = "DefaultOnError")]
1866 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1867 #[serde(default, rename = "_meta")]
1868 pub meta: Option<Meta>,
1869}
1870
1871#[cfg(feature = "unstable_subagents")]
1872impl UnknownStateUpdate {
1873 #[must_use]
1875 pub fn new() -> Self {
1876 Self::default()
1877 }
1878
1879 #[must_use]
1881 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1882 self.meta = meta.into_option();
1883 self
1884 }
1885}
1886
1887#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1892#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1893#[cfg_attr(feature = "schemars", schemars(inline))]
1894#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
1895#[serde(rename_all = "camelCase")]
1896#[non_exhaustive]
1897pub struct OtherStateUpdate {
1898 #[serde(rename = "state")]
1904 pub state: String,
1905 #[serde(flatten)]
1907 pub fields: BTreeMap<String, serde_json::Value>,
1908}
1909
1910impl OtherStateUpdate {
1911 #[must_use]
1913 pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1914 fields.remove("state");
1915 Self {
1916 state: state.into(),
1917 fields,
1918 }
1919 }
1920}
1921
1922impl<'de> Deserialize<'de> for OtherStateUpdate {
1923 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1924 where
1925 D: serde::Deserializer<'de>,
1926 {
1927 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1928 let state = fields
1929 .remove("state")
1930 .ok_or_else(|| serde::de::Error::missing_field("state"))?;
1931 let serde_json::Value::String(state) = state else {
1932 return Err(serde::de::Error::custom("`state` must be a string"));
1933 };
1934
1935 if is_known_state_update(&state) {
1936 return Err(serde::de::Error::custom(format!(
1937 "known state update `{state}` did not match its schema"
1938 )));
1939 }
1940
1941 Ok(Self { state, fields })
1942 }
1943}
1944
1945const KNOWN_STATE_UPDATE_STATES: &[&str] = &[
1946 "running",
1947 "idle",
1948 "requires_action",
1949 #[cfg(feature = "unstable_subagents")]
1950 "unknown",
1951];
1952
1953fn is_known_state_update(state: &str) -> bool {
1954 KNOWN_STATE_UPDATE_STATES.contains(&state)
1955}
1956
1957#[cfg(feature = "schemars")]
1958fn other_state_update_schema(schema: &mut Schema) {
1959 super::schema_util::reject_known_string_discriminators(
1960 schema,
1961 "state",
1962 KNOWN_STATE_UPDATE_STATES,
1963 );
1964}
1965
1966#[serde_as]
1968#[skip_serializing_none]
1969#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1970#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1971#[serde(rename_all = "camelCase")]
1972#[non_exhaustive]
1973pub struct Cost {
1974 pub amount: f64,
1976 pub currency: String,
1979 #[serde_as(deserialize_as = "DefaultOnError")]
1985 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1986 #[serde(default)]
1987 #[serde(rename = "_meta")]
1988 pub meta: Option<Meta>,
1989}
1990
1991impl Cost {
1992 #[must_use]
1994 pub fn new(amount: f64, currency: impl Into<String>) -> Self {
1995 Self {
1996 amount,
1997 currency: currency.into(),
1998 meta: None,
1999 }
2000 }
2001
2002 #[must_use]
2008 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2009 self.meta = meta.into_option();
2010 self
2011 }
2012}
2013
2014#[serde_as]
2016#[skip_serializing_none]
2017#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2018#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2019#[serde(rename_all = "camelCase")]
2020#[non_exhaustive]
2021pub struct ContentChunk {
2022 pub message_id: MessageId,
2027 pub content: ContentBlock,
2029 #[serde_as(deserialize_as = "DefaultOnError")]
2036 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2037 #[serde(default)]
2038 #[serde(rename = "_meta")]
2039 pub meta: Option<Meta>,
2040}
2041
2042impl ContentChunk {
2043 #[must_use]
2045 pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
2046 Self {
2047 content,
2048 message_id: message_id.into(),
2049 meta: None,
2050 }
2051 }
2052
2053 #[must_use]
2059 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2060 self.meta = meta.into_option();
2061 self
2062 }
2063}
2064
2065#[serde_as]
2079#[skip_serializing_none]
2080#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2081#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2082#[serde(rename_all = "camelCase")]
2083#[non_exhaustive]
2084pub struct UserMessage {
2085 pub message_id: MessageId,
2087 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2089 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2090 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2091 pub content: MaybeUndefined<Vec<ContentBlock>>,
2092 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2101 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2102 #[serde(
2103 rename = "_meta",
2104 default,
2105 skip_serializing_if = "MaybeUndefined::is_undefined"
2106 )]
2107 pub meta: MaybeUndefined<Meta>,
2108}
2109
2110impl UserMessage {
2111 #[must_use]
2113 pub fn new(message_id: impl Into<MessageId>) -> Self {
2114 Self {
2115 message_id: message_id.into(),
2116 content: MaybeUndefined::Undefined,
2117 meta: MaybeUndefined::Undefined,
2118 }
2119 }
2120
2121 #[must_use]
2123 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2124 self.content = content.into_maybe_undefined();
2125 self
2126 }
2127
2128 #[must_use]
2133 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2134 self.meta = meta.into_maybe_undefined();
2135 self
2136 }
2137}
2138
2139#[serde_as]
2153#[skip_serializing_none]
2154#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2156#[serde(rename_all = "camelCase")]
2157#[non_exhaustive]
2158pub struct AgentMessage {
2159 pub message_id: MessageId,
2161 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2163 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2164 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2165 pub content: MaybeUndefined<Vec<ContentBlock>>,
2166 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2175 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2176 #[serde(
2177 rename = "_meta",
2178 default,
2179 skip_serializing_if = "MaybeUndefined::is_undefined"
2180 )]
2181 pub meta: MaybeUndefined<Meta>,
2182}
2183
2184impl AgentMessage {
2185 #[must_use]
2187 pub fn new(message_id: impl Into<MessageId>) -> Self {
2188 Self {
2189 message_id: message_id.into(),
2190 content: MaybeUndefined::Undefined,
2191 meta: MaybeUndefined::Undefined,
2192 }
2193 }
2194
2195 #[must_use]
2197 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2198 self.content = content.into_maybe_undefined();
2199 self
2200 }
2201
2202 #[must_use]
2207 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2208 self.meta = meta.into_maybe_undefined();
2209 self
2210 }
2211}
2212
2213#[serde_as]
2227#[skip_serializing_none]
2228#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2230#[serde(rename_all = "camelCase")]
2231#[non_exhaustive]
2232pub struct AgentThought {
2233 pub message_id: MessageId,
2235 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2237 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2238 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2239 pub content: MaybeUndefined<Vec<ContentBlock>>,
2240 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2249 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2250 #[serde(
2251 rename = "_meta",
2252 default,
2253 skip_serializing_if = "MaybeUndefined::is_undefined"
2254 )]
2255 pub meta: MaybeUndefined<Meta>,
2256}
2257
2258impl AgentThought {
2259 #[must_use]
2261 pub fn new(message_id: impl Into<MessageId>) -> Self {
2262 Self {
2263 message_id: message_id.into(),
2264 content: MaybeUndefined::Undefined,
2265 meta: MaybeUndefined::Undefined,
2266 }
2267 }
2268
2269 #[must_use]
2271 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2272 self.content = content.into_maybe_undefined();
2273 self
2274 }
2275
2276 #[must_use]
2281 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2282 self.meta = meta.into_maybe_undefined();
2283 self
2284 }
2285}
2286
2287#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
2294#[serde(transparent)]
2295#[from(forward)]
2296#[non_exhaustive]
2297pub struct MessageId(pub Arc<str>);
2298
2299impl MessageId {
2300 #[must_use]
2302 pub fn new(id: impl Into<Self>) -> Self {
2303 id.into()
2304 }
2305}
2306
2307#[serde_as]
2309#[skip_serializing_none]
2310#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2312#[serde(rename_all = "camelCase")]
2313#[non_exhaustive]
2314pub struct AvailableCommandsUpdate {
2315 #[serde_as(deserialize_as = "VecSkipError<_>")]
2317 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
2318 pub available_commands: Vec<AvailableCommand>,
2319 #[serde_as(deserialize_as = "DefaultOnError")]
2325 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2326 #[serde(default)]
2327 #[serde(rename = "_meta")]
2328 pub meta: Option<Meta>,
2329}
2330
2331impl AvailableCommandsUpdate {
2332 #[must_use]
2334 pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
2335 Self {
2336 available_commands,
2337 meta: None,
2338 }
2339 }
2340
2341 #[must_use]
2347 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2348 self.meta = meta.into_option();
2349 self
2350 }
2351}
2352
2353#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2357#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2358#[serde(tag = "type", rename_all = "snake_case")]
2359#[non_exhaustive]
2360pub enum AvailableCommand {
2361 Command(Command),
2363 #[serde(untagged)]
2374 Other(OtherAvailableCommand),
2375}
2376
2377impl AvailableCommand {
2378 #[must_use]
2380 pub fn name(&self) -> &str {
2381 match self {
2382 Self::Command(c) => &c.name,
2383 Self::Other(c) => &c.name,
2384 }
2385 }
2386
2387 #[must_use]
2389 pub fn description(&self) -> &str {
2390 match self {
2391 Self::Command(c) => &c.description,
2392 Self::Other(c) => &c.description,
2393 }
2394 }
2395
2396 #[must_use]
2402 pub fn meta(&self) -> Option<&Meta> {
2403 match self {
2404 Self::Command(c) => c.meta.as_ref(),
2405 Self::Other(c) => c.fields.get("_meta").and_then(serde_json::Value::as_object),
2406 }
2407 }
2408}
2409
2410#[serde_as]
2412#[skip_serializing_none]
2413#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2414#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2415#[cfg_attr(feature = "schemars", schemars(inline))]
2416#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_schema))]
2417#[serde(rename_all = "camelCase")]
2418#[non_exhaustive]
2419pub struct OtherAvailableCommand {
2420 #[serde(rename = "type")]
2426 pub type_: String,
2427 pub name: String,
2430 pub description: String,
2432 #[serde(flatten)]
2435 pub fields: BTreeMap<String, serde_json::Value>,
2436}
2437
2438impl OtherAvailableCommand {
2439 #[must_use]
2441 pub fn new(
2442 type_: impl Into<String>,
2443 name: impl Into<String>,
2444 description: impl Into<String>,
2445 mut fields: BTreeMap<String, serde_json::Value>,
2446 ) -> Self {
2447 fields.remove("type");
2448 fields.remove("name");
2449 fields.remove("description");
2450 Self {
2451 type_: type_.into(),
2452 name: name.into(),
2453 description: description.into(),
2454 fields,
2455 }
2456 }
2457}
2458
2459impl<'de> Deserialize<'de> for OtherAvailableCommand {
2460 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2461 where
2462 D: serde::Deserializer<'de>,
2463 {
2464 #[derive(Deserialize)]
2465 struct RawOtherAvailableCommand {
2466 #[serde(rename = "type")]
2467 type_: String,
2468 name: String,
2469 description: String,
2470 #[serde(flatten)]
2471 fields: BTreeMap<String, serde_json::Value>,
2472 }
2473
2474 let raw = RawOtherAvailableCommand::deserialize(deserializer)?;
2475 if is_known_available_command_type(&raw.type_) {
2476 return Err(serde::de::Error::custom(format!(
2477 "known available command type `{}` did not match its schema",
2478 raw.type_
2479 )));
2480 }
2481
2482 Ok(Self {
2483 type_: raw.type_,
2484 name: raw.name,
2485 description: raw.description,
2486 fields: raw.fields,
2487 })
2488 }
2489}
2490
2491const KNOWN_AVAILABLE_COMMAND_TYPES: &[&str] = &["command"];
2492
2493fn is_known_available_command_type(type_: &str) -> bool {
2494 KNOWN_AVAILABLE_COMMAND_TYPES.contains(&type_)
2495}
2496
2497#[cfg(feature = "schemars")]
2498fn other_available_command_schema(schema: &mut Schema) {
2499 super::schema_util::reject_known_string_discriminators(
2500 schema,
2501 "type",
2502 KNOWN_AVAILABLE_COMMAND_TYPES,
2503 );
2504}
2505
2506#[serde_as]
2510#[skip_serializing_none]
2511#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2513#[serde(rename_all = "camelCase")]
2514#[non_exhaustive]
2515pub struct Command {
2516 pub name: String,
2520 pub description: String,
2522 #[serde_as(deserialize_as = "DefaultOnError")]
2526 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2527 #[serde(default)]
2528 pub input: Option<AvailableCommandInput>,
2529 #[serde_as(deserialize_as = "DefaultOnError")]
2535 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2536 #[serde(default)]
2537 #[serde(rename = "_meta")]
2538 pub meta: Option<Meta>,
2539}
2540
2541impl Command {
2542 #[must_use]
2544 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
2545 Self {
2546 name: name.into(),
2547 description: description.into(),
2548 input: None,
2549 meta: None,
2550 }
2551 }
2552
2553 #[must_use]
2555 pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
2556 self.input = input.into_option();
2557 self
2558 }
2559
2560 #[must_use]
2566 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2567 self.meta = meta.into_option();
2568 self
2569 }
2570}
2571
2572#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2575#[serde(tag = "type", rename_all = "snake_case")]
2576#[non_exhaustive]
2577pub enum AvailableCommandInput {
2578 #[serde(rename = "text")]
2580 Text(TextCommandInput),
2581 #[serde(untagged)]
2592 Other(OtherAvailableCommandInput),
2593}
2594
2595#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2597#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2598#[cfg_attr(feature = "schemars", schemars(inline))]
2599#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
2600#[serde(rename_all = "camelCase")]
2601#[non_exhaustive]
2602pub struct OtherAvailableCommandInput {
2603 #[serde(rename = "type")]
2609 pub type_: String,
2610 #[serde(flatten)]
2612 pub fields: BTreeMap<String, serde_json::Value>,
2613}
2614
2615impl OtherAvailableCommandInput {
2616 #[must_use]
2618 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2619 fields.remove("type");
2620 Self {
2621 type_: type_.into(),
2622 fields,
2623 }
2624 }
2625}
2626
2627impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
2628 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2629 where
2630 D: serde::Deserializer<'de>,
2631 {
2632 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2633 let type_ = fields
2634 .remove("type")
2635 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2636 let serde_json::Value::String(type_) = type_ else {
2637 return Err(serde::de::Error::custom("`type` must be a string"));
2638 };
2639
2640 if is_known_available_command_input_type(&type_) {
2641 return Err(serde::de::Error::custom(format!(
2642 "known available command input type `{type_}` did not match its schema"
2643 )));
2644 }
2645
2646 Ok(Self { type_, fields })
2647 }
2648}
2649
2650const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
2651
2652fn is_known_available_command_input_type(type_: &str) -> bool {
2653 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
2654}
2655
2656#[cfg(feature = "schemars")]
2657fn other_available_command_input_schema(schema: &mut Schema) {
2658 super::schema_util::reject_known_string_discriminators(
2659 schema,
2660 "type",
2661 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
2662 );
2663}
2664
2665#[serde_as]
2667#[skip_serializing_none]
2668#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2669#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2670#[serde(rename_all = "camelCase")]
2671#[non_exhaustive]
2672pub struct TextCommandInput {
2673 pub hint: String,
2675 #[serde_as(deserialize_as = "DefaultOnError")]
2681 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2682 #[serde(default)]
2683 #[serde(rename = "_meta")]
2684 pub meta: Option<Meta>,
2685}
2686
2687impl TextCommandInput {
2688 #[must_use]
2690 pub fn new(hint: impl Into<String>) -> Self {
2691 Self {
2692 hint: hint.into(),
2693 meta: None,
2694 }
2695 }
2696
2697 #[must_use]
2703 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2704 self.meta = meta.into_option();
2705 self
2706 }
2707}
2708
2709#[serde_as]
2717#[skip_serializing_none]
2718#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2719#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2720#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
2721#[serde(rename_all = "camelCase")]
2722#[non_exhaustive]
2723pub struct RequestPermissionRequest {
2724 pub session_id: SessionId,
2726 pub title: String,
2731 #[serde_as(deserialize_as = "DefaultOnError")]
2737 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2738 #[serde(default)]
2739 pub description: Option<String>,
2740 #[serde(default)]
2744 pub subject: Option<RequestPermissionSubject>,
2745 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
2748 pub options: Vec<PermissionOption>,
2749 #[serde_as(deserialize_as = "DefaultOnError")]
2755 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2756 #[serde(default)]
2757 #[serde(rename = "_meta")]
2758 pub meta: Option<Meta>,
2759}
2760
2761impl RequestPermissionRequest {
2762 #[must_use]
2764 pub fn new(
2765 session_id: impl Into<SessionId>,
2766 title: impl Into<String>,
2767 options: Vec<PermissionOption>,
2768 ) -> Self {
2769 Self {
2770 session_id: session_id.into(),
2771 title: title.into(),
2772 description: None,
2773 subject: None,
2774 options,
2775 meta: None,
2776 }
2777 }
2778
2779 #[must_use]
2781 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
2782 self.description = description.into_option();
2783 self
2784 }
2785
2786 #[must_use]
2788 pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
2789 self.subject = subject.into_option();
2790 self
2791 }
2792
2793 #[must_use]
2799 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2800 self.meta = meta.into_option();
2801 self
2802 }
2803}
2804
2805#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2807#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2808#[serde(tag = "type", rename_all = "snake_case")]
2809#[non_exhaustive]
2810pub enum RequestPermissionSubject {
2811 ToolCall(Box<ToolCallPermissionSubject>),
2813 Command(CommandPermissionSubject),
2815 #[serde(untagged)]
2826 Other(OtherRequestPermissionSubject),
2827}
2828
2829impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
2830 fn from(subject: ToolCallPermissionSubject) -> Self {
2831 Self::ToolCall(Box::new(subject))
2832 }
2833}
2834
2835impl From<ToolCallUpdate> for RequestPermissionSubject {
2836 fn from(tool_call: ToolCallUpdate) -> Self {
2837 ToolCallPermissionSubject::new(tool_call).into()
2838 }
2839}
2840
2841impl From<CommandPermissionSubject> for RequestPermissionSubject {
2842 fn from(subject: CommandPermissionSubject) -> Self {
2843 Self::Command(subject)
2844 }
2845}
2846
2847#[serde_as]
2849#[skip_serializing_none]
2850#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2851#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2852#[serde(rename_all = "camelCase")]
2853#[non_exhaustive]
2854pub struct ToolCallPermissionSubject {
2855 pub tool_call: ToolCallUpdate,
2861 #[serde_as(deserialize_as = "DefaultOnError")]
2867 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2868 #[serde(default)]
2869 #[serde(rename = "_meta")]
2870 pub meta: Option<Meta>,
2871}
2872
2873impl ToolCallPermissionSubject {
2874 #[must_use]
2876 pub fn new(tool_call: ToolCallUpdate) -> Self {
2877 Self {
2878 tool_call,
2879 meta: None,
2880 }
2881 }
2882
2883 #[must_use]
2889 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2890 self.meta = meta.into_option();
2891 self
2892 }
2893}
2894
2895#[serde_as]
2897#[skip_serializing_none]
2898#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2899#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2900#[serde(rename_all = "camelCase")]
2901#[non_exhaustive]
2902pub struct CommandPermissionSubject {
2903 pub command: String,
2905 pub cwd: AbsolutePath,
2907 #[serde_as(deserialize_as = "DefaultOnError")]
2909 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2910 #[serde(default)]
2911 pub tool_call_id: Option<ToolCallId>,
2912 #[serde_as(deserialize_as = "DefaultOnError")]
2914 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2915 #[serde(default)]
2916 pub terminal_id: Option<TerminalId>,
2917 #[serde_as(deserialize_as = "DefaultOnError")]
2923 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2924 #[serde(default)]
2925 #[serde(rename = "_meta")]
2926 pub meta: Option<Meta>,
2927}
2928
2929impl CommandPermissionSubject {
2930 #[must_use]
2932 pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
2933 Self {
2934 command: command.into(),
2935 cwd: cwd.into(),
2936 tool_call_id: None,
2937 terminal_id: None,
2938 meta: None,
2939 }
2940 }
2941
2942 #[must_use]
2944 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
2945 self.tool_call_id = tool_call_id.into_option();
2946 self
2947 }
2948
2949 #[must_use]
2951 pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
2952 self.terminal_id = terminal_id.into_option();
2953 self
2954 }
2955
2956 #[must_use]
2958 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2959 self.meta = meta.into_option();
2960 self
2961 }
2962}
2963
2964#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2966#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2967#[cfg_attr(feature = "schemars", schemars(inline))]
2968#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
2969#[serde(rename_all = "camelCase")]
2970#[non_exhaustive]
2971pub struct OtherRequestPermissionSubject {
2972 #[serde(rename = "type")]
2978 pub type_: String,
2979 #[serde(flatten)]
2981 pub fields: BTreeMap<String, serde_json::Value>,
2982}
2983
2984impl OtherRequestPermissionSubject {
2985 #[must_use]
2987 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2988 fields.remove("type");
2989 Self {
2990 type_: type_.into(),
2991 fields,
2992 }
2993 }
2994}
2995
2996impl<'de> Deserialize<'de> for OtherRequestPermissionSubject {
2997 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2998 where
2999 D: serde::Deserializer<'de>,
3000 {
3001 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
3002 let type_ = fields
3003 .remove("type")
3004 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
3005 let serde_json::Value::String(type_) = type_ else {
3006 return Err(serde::de::Error::custom("`type` must be a string"));
3007 };
3008
3009 if is_known_request_permission_subject_type(&type_) {
3010 return Err(serde::de::Error::custom(format!(
3011 "known request permission subject `{type_}` did not match its schema"
3012 )));
3013 }
3014
3015 Ok(Self { type_, fields })
3016 }
3017}
3018
3019fn is_known_request_permission_subject_type(type_: &str) -> bool {
3020 matches!(type_, "tool_call" | "command")
3021}
3022
3023#[cfg(feature = "schemars")]
3024fn other_request_permission_subject_schema(schema: &mut Schema) {
3025 super::schema_util::reject_known_string_discriminators(
3026 schema,
3027 "type",
3028 &["tool_call", "command"],
3029 );
3030}
3031
3032#[serde_as]
3034#[skip_serializing_none]
3035#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3036#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3037#[serde(rename_all = "camelCase")]
3038#[non_exhaustive]
3039pub struct PermissionOption {
3040 pub option_id: PermissionOptionId,
3042 pub name: String,
3044 pub kind: PermissionOptionKind,
3046 #[serde_as(deserialize_as = "DefaultOnError")]
3052 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3053 #[serde(default)]
3054 #[serde(rename = "_meta")]
3055 pub meta: Option<Meta>,
3056}
3057
3058impl PermissionOption {
3059 #[must_use]
3061 pub fn new(
3062 option_id: impl Into<PermissionOptionId>,
3063 name: impl Into<String>,
3064 kind: PermissionOptionKind,
3065 ) -> Self {
3066 Self {
3067 option_id: option_id.into(),
3068 name: name.into(),
3069 kind,
3070 meta: None,
3071 }
3072 }
3073
3074 #[must_use]
3080 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3081 self.meta = meta.into_option();
3082 self
3083 }
3084}
3085
3086#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3088#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
3089#[serde(transparent)]
3090#[from(forward)]
3091#[non_exhaustive]
3092pub struct PermissionOptionId(pub Arc<str>);
3093
3094impl PermissionOptionId {
3095 #[must_use]
3097 pub fn new(id: impl Into<Self>) -> Self {
3098 id.into()
3099 }
3100}
3101
3102#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3107#[serde(rename_all = "snake_case")]
3108#[non_exhaustive]
3109pub enum PermissionOptionKind {
3110 AllowOnce,
3112 AllowAlways,
3114 RejectOnce,
3116 RejectAlways,
3118 #[serde(untagged)]
3124 Other(String),
3125}
3126
3127#[serde_as]
3129#[skip_serializing_none]
3130#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3132#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
3133#[serde(rename_all = "camelCase")]
3134#[non_exhaustive]
3135pub struct RequestPermissionResponse {
3136 pub outcome: RequestPermissionOutcome,
3138 #[serde_as(deserialize_as = "DefaultOnError")]
3144 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3145 #[serde(default)]
3146 #[serde(rename = "_meta")]
3147 pub meta: Option<Meta>,
3148}
3149
3150impl RequestPermissionResponse {
3151 #[must_use]
3153 pub fn new(outcome: RequestPermissionOutcome) -> Self {
3154 Self {
3155 outcome,
3156 meta: None,
3157 }
3158 }
3159
3160 #[must_use]
3166 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3167 self.meta = meta.into_option();
3168 self
3169 }
3170}
3171
3172#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3175#[serde(tag = "outcome", rename_all = "snake_case")]
3176#[non_exhaustive]
3177pub enum RequestPermissionOutcome {
3178 Cancelled(CancelledPermissionOutcome),
3189 #[serde(rename_all = "camelCase")]
3194 Selected(SelectedPermissionOutcome),
3195 #[serde(untagged)]
3206 Other(OtherRequestPermissionOutcome),
3207}
3208
3209#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3215#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3216#[cfg_attr(feature = "schemars", schemars(inline))]
3217#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
3218#[serde(rename_all = "camelCase")]
3219#[non_exhaustive]
3220pub struct OtherRequestPermissionOutcome {
3221 pub outcome: String,
3227 #[serde(flatten)]
3229 pub fields: BTreeMap<String, serde_json::Value>,
3230}
3231
3232impl OtherRequestPermissionOutcome {
3233 #[must_use]
3235 pub fn new(
3236 outcome: impl Into<String>,
3237 mut fields: BTreeMap<String, serde_json::Value>,
3238 ) -> Self {
3239 fields.remove("outcome");
3240 Self {
3241 outcome: outcome.into(),
3242 fields,
3243 }
3244 }
3245}
3246
3247impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
3248 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3249 where
3250 D: serde::Deserializer<'de>,
3251 {
3252 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
3253 let outcome = fields
3254 .remove("outcome")
3255 .ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
3256 let serde_json::Value::String(outcome) = outcome else {
3257 return Err(serde::de::Error::custom("`outcome` must be a string"));
3258 };
3259
3260 if is_known_request_permission_outcome(&outcome) {
3261 return Err(serde::de::Error::custom(format!(
3262 "known request permission outcome `{outcome}` did not match its schema"
3263 )));
3264 }
3265
3266 Ok(Self { outcome, fields })
3267 }
3268}
3269
3270fn is_known_request_permission_outcome(outcome: &str) -> bool {
3271 matches!(outcome, "cancelled" | "selected")
3272}
3273
3274#[cfg(feature = "schemars")]
3275fn other_request_permission_outcome_schema(schema: &mut Schema) {
3276 super::schema_util::reject_known_string_discriminators(
3277 schema,
3278 "outcome",
3279 &["cancelled", "selected"],
3280 );
3281}
3282
3283#[serde_as]
3285#[skip_serializing_none]
3286#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3287#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3288#[serde(rename_all = "camelCase")]
3289#[non_exhaustive]
3290pub struct CancelledPermissionOutcome {
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 CancelledPermissionOutcome {
3304 #[must_use]
3306 pub fn new() -> Self {
3307 Self::default()
3308 }
3309
3310 #[must_use]
3316 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3317 self.meta = meta.into_option();
3318 self
3319 }
3320}
3321
3322impl From<CancelledPermissionOutcome> for RequestPermissionOutcome {
3323 fn from(outcome: CancelledPermissionOutcome) -> Self {
3324 Self::Cancelled(outcome)
3325 }
3326}
3327
3328#[serde_as]
3330#[skip_serializing_none]
3331#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3333#[serde(rename_all = "camelCase")]
3334#[non_exhaustive]
3335pub struct SelectedPermissionOutcome {
3336 pub option_id: PermissionOptionId,
3338 #[serde_as(deserialize_as = "DefaultOnError")]
3344 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3345 #[serde(default)]
3346 #[serde(rename = "_meta")]
3347 pub meta: Option<Meta>,
3348}
3349
3350impl SelectedPermissionOutcome {
3351 #[must_use]
3353 pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
3354 Self {
3355 option_id: option_id.into(),
3356 meta: None,
3357 }
3358 }
3359
3360 #[must_use]
3366 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3367 self.meta = meta.into_option();
3368 self
3369 }
3370}
3371
3372#[serde_as]
3381#[skip_serializing_none]
3382#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3383#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3384#[serde(rename_all = "camelCase")]
3385#[non_exhaustive]
3386pub struct ClientCapabilities {
3387 #[serde_as(deserialize_as = "DefaultOnError")]
3394 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3395 #[serde(default)]
3396 pub auth: Option<ClientAuthCapabilities>,
3397 #[serde_as(deserialize_as = "DefaultOnError")]
3403 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3404 #[serde(default)]
3405 pub elicitation: Option<ElicitationCapabilities>,
3406
3407 #[serde_as(deserialize_as = "DefaultOnError")]
3413 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3414 #[serde(default)]
3415 #[serde(rename = "_meta")]
3416 pub meta: Option<Meta>,
3417}
3418
3419impl ClientCapabilities {
3420 #[must_use]
3422 pub fn new() -> Self {
3423 Self::default()
3424 }
3425
3426 #[must_use]
3430 pub fn auth(mut self, auth: impl IntoOption<ClientAuthCapabilities>) -> Self {
3431 self.auth = auth.into_option();
3432 self
3433 }
3434
3435 #[must_use]
3438 pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
3439 self.elicitation = elicitation.into_option();
3440 self
3441 }
3442
3443 #[must_use]
3449 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3450 self.meta = meta.into_option();
3451 self
3452 }
3453}
3454
3455#[serde_as]
3461#[skip_serializing_none]
3462#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3463#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3464#[serde(rename_all = "camelCase")]
3465#[non_exhaustive]
3466pub struct ClientAuthCapabilities {
3467 #[serde_as(deserialize_as = "DefaultOnError")]
3474 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3475 #[serde(default)]
3476 pub terminal: Option<TerminalAuthCapabilities>,
3477 #[serde_as(deserialize_as = "DefaultOnError")]
3483 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3484 #[serde(default)]
3485 #[serde(rename = "_meta")]
3486 pub meta: Option<Meta>,
3487}
3488
3489impl ClientAuthCapabilities {
3490 #[must_use]
3492 pub fn new() -> Self {
3493 Self::default()
3494 }
3495
3496 #[must_use]
3504 pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
3505 self.terminal = terminal.into_option();
3506 self
3507 }
3508
3509 #[must_use]
3515 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3516 self.meta = meta.into_option();
3517 self
3518 }
3519}
3520
3521#[serde_as]
3527#[skip_serializing_none]
3528#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3529#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3530#[non_exhaustive]
3531pub struct TerminalAuthCapabilities {
3532 #[serde_as(deserialize_as = "DefaultOnError")]
3538 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3539 #[serde(default)]
3540 #[serde(rename = "_meta")]
3541 pub meta: Option<Meta>,
3542}
3543
3544impl TerminalAuthCapabilities {
3545 #[must_use]
3547 pub fn new() -> Self {
3548 Self::default()
3549 }
3550
3551 #[must_use]
3557 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3558 self.meta = meta.into_option();
3559 self
3560 }
3561}
3562
3563#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3569#[non_exhaustive]
3570pub struct ClientMethodNames {
3571 pub session_request_permission: &'static str,
3573 pub session_update: &'static str,
3575 #[cfg(feature = "unstable_mcp_over_acp")]
3577 pub mcp_message: &'static str,
3578 pub elicitation_create: &'static str,
3580 pub elicitation_complete: &'static str,
3582}
3583
3584pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
3586 session_update: SESSION_UPDATE_NOTIFICATION,
3587 session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
3588 #[cfg(feature = "unstable_mcp_over_acp")]
3589 mcp_message: MCP_MESSAGE_METHOD_NAME,
3590 elicitation_create: ELICITATION_CREATE_METHOD_NAME,
3591 elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
3592};
3593
3594pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
3596pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
3598pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
3600pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
3602
3603#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3610#[derive(Clone, Debug, Serialize, Deserialize)]
3611#[serde(untagged)]
3612#[cfg_attr(feature = "schemars", schemars(inline))]
3613#[non_exhaustive]
3614pub enum AgentRequest {
3615 RequestPermissionRequest(Box<RequestPermissionRequest>),
3626 CreateElicitationRequest(Box<CreateElicitationRequest>),
3630 #[cfg(feature = "unstable_mcp_over_acp")]
3636 MessageMcpRequest(Box<MessageMcpRequest>),
3637 ExtMethodRequest(Box<ExtRequest>),
3645}
3646
3647impl AgentRequest {
3648 #[must_use]
3650 pub fn method(&self) -> &str {
3651 match self {
3652 Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
3653 Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
3654 #[cfg(feature = "unstable_mcp_over_acp")]
3655 Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
3656 Self::ExtMethodRequest(ext_request) => &ext_request.method,
3657 }
3658 }
3659}
3660
3661#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3668#[derive(Clone, Debug, Serialize, Deserialize)]
3669#[serde(untagged)]
3670#[cfg_attr(feature = "schemars", schemars(inline))]
3671#[non_exhaustive]
3672pub enum ClientResponse {
3673 RequestPermissionResponse(Box<RequestPermissionResponse>),
3675 CreateElicitationResponse(Box<CreateElicitationResponse>),
3677 #[cfg(feature = "unstable_mcp_over_acp")]
3679 MessageMcpResponse(Box<MessageMcpResponse>),
3680 ExtMethodResponse(Box<ExtResponse>),
3682}
3683
3684#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3691#[derive(Clone, Debug, Serialize, Deserialize)]
3692#[serde(untagged)]
3693#[cfg_attr(feature = "schemars", schemars(inline))]
3694#[non_exhaustive]
3695pub enum AgentNotification {
3696 UpdateSessionNotification(Box<UpdateSessionNotification>),
3709 CompleteElicitationNotification(Box<CompleteElicitationNotification>),
3713 ExtNotification(Box<ExtNotification>),
3721}
3722
3723impl AgentNotification {
3724 #[must_use]
3726 pub fn method(&self) -> &str {
3727 match self {
3728 Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
3729 Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
3730 Self::ExtNotification(ext_notification) => &ext_notification.method,
3731 }
3732 }
3733}
3734
3735#[cfg(test)]
3736mod tests {
3737
3738 #[test]
3739 fn replacement_lists_reject_non_arrays_instead_of_clearing() {
3740 use serde_json::json;
3741 for (update, key) in [
3742 ("config_option_update", "configOptions"),
3743 ("available_commands_update", "availableCommands"),
3744 ] {
3745 let empty = json!({ "sessionUpdate": update, key: [] });
3746 serde_json::from_value::<super::SessionUpdate>(empty).unwrap();
3747 for bad in [json!("oops"), json!(null), json!({})] {
3748 let wire = json!({ "sessionUpdate": update, key: bad });
3749 assert!(
3750 serde_json::from_value::<super::SessionUpdate>(wire).is_err(),
3751 "{update} {bad}"
3752 );
3753 }
3754 }
3755 let plan = |entries: serde_json::Value| {
3756 json!({ "sessionUpdate": "plan_update",
3757 "plan": { "type": "items", "planId": "p", "entries": entries } })
3758 };
3759 serde_json::from_value::<super::SessionUpdate>(plan(json!([]))).unwrap();
3760 assert!(serde_json::from_value::<super::SessionUpdate>(plan(json!("oops"))).is_err());
3761 let skipped =
3763 plan(json!([42, { "content": "a", "priority": "high", "status": "pending" }]));
3764 let super::SessionUpdate::PlanUpdate(update) =
3765 serde_json::from_value::<super::SessionUpdate>(skipped).unwrap()
3766 else {
3767 panic!("expected plan_update");
3768 };
3769 let crate::v2::PlanUpdateContent::Items(items) = update.plan else {
3770 panic!("expected items");
3771 };
3772 assert_eq!(items.entries.len(), 1);
3773 }
3774
3775 #[test]
3776 fn other_available_command_preserves_meta_verbatim() {
3777 for meta in [serde_json::json!({ "k": 1 }), serde_json::json!("bad")] {
3778 let wire = serde_json::json!({ "type": "_custom", "name": "c",
3779 "description": "d", "_meta": meta, "extra": true });
3780 let command: super::AvailableCommand = serde_json::from_value(wire.clone()).unwrap();
3781 assert_eq!(command.meta(), meta.as_object());
3782 assert_eq!(serde_json::to_value(&command).unwrap(), wire);
3783 }
3784 }
3785
3786 #[test]
3787 fn cancelled_permission_outcome_wire_format() {
3788 let outcome: RequestPermissionOutcome =
3789 serde_json::from_value(serde_json::json!({ "outcome": "cancelled" })).unwrap();
3790 assert_eq!(outcome, CancelledPermissionOutcome::new().into());
3791 assert_eq!(
3792 serde_json::to_value(&outcome).unwrap(),
3793 serde_json::json!({ "outcome": "cancelled" })
3794 );
3795
3796 let with_meta: RequestPermissionOutcome = CancelledPermissionOutcome::new()
3797 .meta(serde_json::Map::from_iter([(
3798 "k".to_string(),
3799 serde_json::json!(1),
3800 )]))
3801 .into();
3802 let json = serde_json::to_value(&with_meta).unwrap();
3803 assert_eq!(
3804 json,
3805 serde_json::json!({ "outcome": "cancelled", "_meta": { "k": 1 } })
3806 );
3807 assert_eq!(
3808 serde_json::from_value::<RequestPermissionOutcome>(json).unwrap(),
3809 with_meta
3810 );
3811 }
3812
3813 use super::*;
3814
3815 #[cfg(feature = "unstable_subagents")]
3816 #[test]
3817 fn subagent_update_serializes_as_upsert() {
3818 use serde_json::json;
3819
3820 let announced =
3821 SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
3822 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
3823 ));
3824 let wire = json!({
3825 "sessionUpdate": "subagent_update",
3826 "sessionId": "sess_child_1",
3827 "capabilities": { "cancel": {} }
3828 });
3829 assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
3830 assert_eq!(
3831 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
3832 announced
3833 );
3834
3835 let minimal = SubagentUpdate::new("sess_child_1");
3836 assert_eq!(
3837 serde_json::to_value(&minimal).unwrap(),
3838 json!({
3839 "sessionId": "sess_child_1"
3840 })
3841 );
3842 assert!(minimal.capabilities.is_undefined());
3843 assert!(minimal.state.is_undefined());
3844 assert!(minimal.meta.is_undefined());
3845
3846 let cleared_wire = json!({
3848 "sessionId": "sess_child_1",
3849 "capabilities": null,
3850 "state": null,
3851 "_meta": null
3852 });
3853 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3854 assert!(cleared.capabilities.is_null());
3855 assert!(cleared.state.is_null());
3856 assert!(cleared.meta.is_null());
3857 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3858
3859 let revoked: SubagentUpdate = serde_json::from_value(json!({
3860 "sessionId": "sess_child_1",
3861 "capabilities": {}
3862 }))
3863 .unwrap();
3864 assert_eq!(
3865 revoked.capabilities,
3866 MaybeUndefined::Value(SubagentSessionCapabilities::new())
3867 );
3868 assert!(
3869 matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
3870 );
3871
3872 assert!(
3873 serde_json::from_value::<SessionUpdate>(json!({
3874 "sessionUpdate": "subagent_update"
3875 }))
3876 .is_err()
3877 );
3878 }
3879
3880 #[cfg(feature = "unstable_subagents")]
3881 #[test]
3882 fn subagent_cancel_capability_is_optional_object() {
3883 use serde_json::json;
3884
3885 for wire in [
3886 json!({}),
3887 json!({"cancel": null}),
3888 json!({"cancel": true}),
3889 json!({"cancel": false}),
3890 ] {
3891 let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
3892 assert!(child.cancel.is_none());
3893 assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
3894 }
3895 let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
3896 assert_eq!(
3897 serde_json::to_value(&enabled).unwrap(),
3898 json!({"cancel": {}})
3899 );
3900 assert_eq!(
3901 serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
3902 enabled
3903 );
3904 let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
3905 let with_meta =
3906 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
3907 let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
3908 assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
3909 assert_eq!(
3910 serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
3911 with_meta
3912 );
3913 assert!(enabled.cancel.is_some());
3914 assert!(enabled.cancel(None).cancel.is_none());
3915 let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
3916 assert_eq!(
3917 serde_json::to_value(removed).unwrap(),
3918 json!({"sessionId": "child", "capabilities": {}})
3919 );
3920 }
3921
3922 #[cfg(feature = "unstable_subagents")]
3923 #[test]
3924 fn subagent_display_metadata_uses_patch_semantics() {
3925 use serde_json::json;
3926
3927 let update = SubagentUpdate::new("child")
3928 .title("Test investigator".to_string())
3929 .description("Investigates platform-specific test failures.".to_string());
3930 let wire = json!({
3931 "sessionId": "child",
3932 "title": "Test investigator",
3933 "description": "Investigates platform-specific test failures."
3934 });
3935 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3936 assert_eq!(
3937 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
3938 update
3939 );
3940
3941 let minimal = SubagentUpdate::new("child");
3942 assert!(minimal.title.is_undefined());
3943 assert!(minimal.description.is_undefined());
3944 assert_eq!(
3945 serde_json::to_value(&minimal).unwrap(),
3946 json!({"sessionId": "child"})
3947 );
3948 let cleared_wire = json!({
3949 "sessionId": "child", "title": null, "description": null
3950 });
3951 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3952 assert!(cleared.title.is_null());
3953 assert!(cleared.description.is_null());
3954 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
3955
3956 let title_only: SubagentUpdate = serde_json::from_value(json!({
3957 "sessionId": "child", "title": "Updated title"
3958 }))
3959 .unwrap();
3960 assert_eq!(
3961 title_only.title,
3962 MaybeUndefined::Value("Updated title".to_string())
3963 );
3964 assert!(title_only.description.is_undefined());
3965 let malformed: SubagentUpdate = serde_json::from_value(json!({
3966 "sessionId": "child", "title": false, "description": false
3967 }))
3968 .unwrap();
3969 assert_eq!(malformed, minimal);
3970 }
3971
3972 #[cfg(feature = "unstable_subagents")]
3973 #[test]
3974 fn subagent_directories_use_strict_patch_semantics() {
3975 use serde_json::json;
3976
3977 let update = SubagentUpdate::new("child")
3978 .cwd("/home/user/project/.worktrees/investigator")
3979 .additional_directories(vec![]);
3980 let wire = json!({
3981 "sessionId": "child",
3982 "cwd": "/home/user/project/.worktrees/investigator",
3983 "additionalDirectories": []
3984 });
3985 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3986 assert_eq!(
3987 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
3988 update
3989 );
3990
3991 let inherited: SubagentUpdate =
3992 serde_json::from_value(json!({"sessionId": "child"})).unwrap();
3993 assert!(inherited.cwd.is_undefined());
3994 assert!(inherited.additional_directories.is_undefined());
3995
3996 let cleared_wire = json!({
3997 "sessionId": "child", "cwd": null, "additionalDirectories": null
3998 });
3999 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4000 assert!(cleared.cwd.is_null());
4001 assert!(cleared.additional_directories.is_null());
4002 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4003
4004 for malformed in [
4005 json!({"sessionId": "child", "cwd": false}),
4006 json!({"sessionId": "child", "additionalDirectories": "/tmp/wt"}),
4007 json!({"sessionId": "child", "additionalDirectories": ["/tmp/wt", false]}),
4008 ] {
4009 assert!(
4010 serde_json::from_value::<SubagentUpdate>(malformed.clone()).is_err(),
4011 "{malformed}"
4012 );
4013 }
4014 }
4015
4016 #[cfg(feature = "unstable_subagents")]
4017 #[test]
4018 fn subagent_notification_keeps_parent_and_child_ids_nested() {
4019 use serde_json::json;
4020
4021 let notification = UpdateSessionNotification::new(
4022 "parent",
4023 SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
4024 );
4025 let wire = json!({
4026 "sessionId": "parent",
4027 "update": {
4028 "sessionUpdate": "subagent_update",
4029 "sessionId": "child"
4030 }
4031 });
4032 assert_eq!(serde_json::to_value(¬ification).unwrap(), wire);
4033 assert_eq!(
4034 serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
4035 notification
4036 );
4037 for malformed in [
4038 json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
4039 json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
4040 ] {
4041 assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
4042 }
4043 }
4044
4045 #[cfg(feature = "unstable_subagents")]
4046 #[test]
4047 fn subagent_mirrors_normal_child_state_notifications() {
4048 use serde_json::json;
4049
4050 let association = SubagentUpdate::new("sess_child");
4051 for state in [
4052 StateUpdate::Running(RunningStateUpdate::new()),
4053 StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
4054 StateUpdate::Unknown(UnknownStateUpdate::new()),
4055 StateUpdate::Running(RunningStateUpdate::new()),
4056 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
4057 StateUpdate::Running(RunningStateUpdate::new()),
4058 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
4059 ] {
4060 let child_notification = UpdateSessionNotification::new(
4061 association.session_id.clone(),
4062 SessionUpdate::StateUpdate(state.clone()),
4063 );
4064 let parent_notification = UpdateSessionNotification::new(
4065 "sess_parent",
4066 SessionUpdate::SubagentUpdate(association.clone().state(state)),
4067 );
4068 let child_wire = serde_json::to_value(&child_notification).unwrap();
4069 let parent_wire = serde_json::to_value(&parent_notification).unwrap();
4070 assert_eq!(child_wire["sessionId"], json!("sess_child"));
4071 assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
4072 assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
4073 assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
4074 let mut child_snapshot = child_wire["update"].clone();
4075 child_snapshot
4076 .as_object_mut()
4077 .unwrap()
4078 .remove("sessionUpdate");
4079 assert_eq!(parent_wire["update"]["state"], child_snapshot);
4080 assert_eq!(
4081 serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
4082 child_notification
4083 );
4084 assert_eq!(
4085 serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
4086 parent_notification
4087 );
4088 }
4089 }
4090
4091 #[cfg(feature = "unstable_subagents")]
4092 #[test]
4093 fn subagent_state_uses_whole_snapshot_patch_semantics() {
4094 use serde_json::json;
4095
4096 let minimal = SubagentUpdate::new("child");
4097 assert!(minimal.state.is_undefined());
4098 assert_eq!(
4099 serde_json::to_value(&minimal).unwrap(),
4100 json!({"sessionId": "child"})
4101 );
4102
4103 let cleared = minimal.clone().state(None::<StateUpdate>);
4104 assert!(cleared.state.is_null());
4105 assert_eq!(
4106 serde_json::to_value(&cleared).unwrap(),
4107 json!({"sessionId": "child", "state": null})
4108 );
4109 assert_eq!(
4110 serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
4111 .unwrap(),
4112 cleared
4113 );
4114
4115 for snapshot in [
4116 json!({"state": "running", "_meta": {"source": "worker"}}),
4117 json!({"state": "idle", "stopReason": "end_turn"}),
4118 json!({"state": "requires_action"}),
4119 json!({"state": "unknown"}),
4120 json!({"state": "_custom", "detail": {"phase": 2}}),
4121 ] {
4122 let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
4123 let update = minimal.clone().state(state.clone());
4124 let wire = json!({"sessionId": "child", "state": snapshot});
4125 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4126 assert_eq!(
4127 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4128 update
4129 );
4130 assert_eq!(update.state, MaybeUndefined::Value(state));
4131 }
4132
4133 let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
4136 IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
4137 ));
4138 let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
4139 assert_eq!(
4140 serde_json::to_value(running).unwrap(),
4141 json!({"sessionId": "child", "state": {"state": "running"}})
4142 );
4143 for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
4144 let parsed: SubagentUpdate =
4145 serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
4146 assert_eq!(parsed, minimal);
4147 }
4148 }
4149
4150 #[cfg(feature = "unstable_subagents")]
4151 #[test]
4152 fn unknown_activity_is_a_known_state_update() {
4153 use serde_json::json;
4154
4155 let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
4156 UnknownStateUpdate::new().meta(
4157 [("source".into(), json!("worker"))]
4158 .into_iter()
4159 .collect::<Meta>(),
4160 ),
4161 ));
4162 let wire = json!({
4163 "sessionUpdate": "state_update",
4164 "state": "unknown",
4165 "_meta": { "source": "worker" }
4166 });
4167 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4168 assert_eq!(
4169 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4170 update
4171 );
4172
4173 for meta in [json!(null), json!(false)] {
4174 let state: StateUpdate = serde_json::from_value(json!({
4175 "state": "unknown",
4176 "_meta": meta
4177 }))
4178 .unwrap();
4179 assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
4180 assert_eq!(
4181 serde_json::to_value(state).unwrap(),
4182 json!({"state": "unknown"})
4183 );
4184 }
4185 assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
4186 }
4187
4188 #[cfg(not(feature = "unstable_subagents"))]
4189 #[test]
4190 fn unknown_activity_is_preserved_without_subagents_feature() {
4191 let wire = serde_json::json!({
4192 "sessionUpdate": "state_update",
4193 "state": "unknown",
4194 "_meta": { "source": "worker" }
4195 });
4196 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4197 let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
4198 panic!("expected unrecognized state payload");
4199 };
4200 assert_eq!(state.state, "unknown");
4201 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4202 }
4203
4204 #[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
4205 #[test]
4206 fn subagent_schema_carries_optional_nullable_state_snapshot() {
4207 use serde_json::json;
4208
4209 let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
4210 let properties = schema["properties"].as_object().unwrap();
4211 assert!(properties.contains_key("sessionId"));
4212 assert!(properties.contains_key("title"));
4213 assert!(properties.contains_key("description"));
4214 assert!(properties.contains_key("capabilities"));
4215 assert!(properties.contains_key("state"));
4216 assert!(properties.contains_key("_meta"));
4217 assert!(!properties.contains_key("name"));
4218 assert!(!properties.contains_key("task"));
4219 assert_eq!(schema["required"], json!(["sessionId"]));
4220 assert_eq!(
4221 properties["state"]["x-deserialize-default-on-error"],
4222 json!(true)
4223 );
4224 let state_variants = properties["state"]["anyOf"].as_array().unwrap();
4225 assert!(state_variants.contains(&json!({"type": "null"})));
4226 assert!(
4227 state_variants
4228 .iter()
4229 .any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
4230 );
4231 let child =
4232 serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
4233 let variants = child["properties"]["cancel"]["anyOf"]
4234 .as_array()
4235 .expect("cancel must allow the capability object or null");
4236 assert!(variants.contains(&json!({"type": "null"})));
4237 assert!(
4238 variants
4239 .iter()
4240 .any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
4241 );
4242 assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
4243 let cancel =
4244 serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
4245 assert_eq!(cancel["type"], "object");
4246 assert!(
4247 !child["required"]
4248 .as_array()
4249 .is_some_and(|required| required.contains(&json!("cancel")))
4250 );
4251 }
4252
4253 #[cfg(not(feature = "unstable_subagents"))]
4254 #[test]
4255 fn unsupported_subagent_update_is_preserved() {
4256 let wire = serde_json::json!({
4257 "sessionUpdate": "subagent_update",
4258 "sessionId": "sess_child",
4259 "capabilities": { "cancel": {} }
4260 });
4261 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4262 assert!(matches!(&parsed, SessionUpdate::Other(_)));
4263 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4264 }
4265
4266 #[test]
4267 fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
4268 use serde_json::json;
4269
4270 let mut meta = Meta::new();
4271 meta.insert("source".into(), json!("fallback"));
4272 let v2_notice = SessionUpdate::Notice(
4273 Notice::new(NoticeSeverity::Error, "Provider degraded")
4274 .description("Requests may take longer than usual.")
4275 .meta(meta.clone()),
4276 );
4277 let expected = json!({
4278 "sessionUpdate": "notice",
4279 "severity": "error",
4280 "title": "Provider degraded",
4281 "description": "Requests may take longer than usual.",
4282 "_meta": { "source": "fallback" }
4283 });
4284 assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
4285
4286 let v1_notice = crate::v1::SessionUpdate::Notice(
4287 crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
4288 .description("Requests may take longer than usual.")
4289 .meta(meta),
4290 );
4291 assert_eq!(
4292 serde_json::to_value(v2_notice).unwrap(),
4293 serde_json::to_value(v1_notice).unwrap()
4294 );
4295
4296 let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
4297 "sessionUpdate": "notice",
4298 "severity": "critical",
4299 "title": "Provider degraded",
4300 "description": null,
4301 "_meta": null
4302 }))
4303 .unwrap() else {
4304 panic!("expected notice");
4305 };
4306
4307 assert_eq!(
4308 notice.severity,
4309 NoticeSeverity::Other("critical".to_string())
4310 );
4311 assert_eq!(notice.description, None);
4312 assert_eq!(notice.meta, None);
4313 assert_eq!(
4314 serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
4315 json!({
4316 "sessionUpdate": "notice",
4317 "severity": "critical",
4318 "title": "Provider degraded"
4319 })
4320 );
4321 }
4322
4323 #[test]
4324 fn malformed_known_notice_is_not_hidden_as_unknown() {
4325 use serde_json::json;
4326
4327 for malformed in [
4328 json!({
4329 "sessionUpdate": "notice",
4330 "severity": "warning",
4331 "title": 42
4332 }),
4333 json!({
4334 "sessionUpdate": "notice",
4335 "severity": 42,
4336 "title": "MCP server unavailable"
4337 }),
4338 json!({
4339 "sessionUpdate": "notice",
4340 "severity": "warning"
4341 }),
4342 json!({
4343 "sessionUpdate": "notice",
4344 "severity": "warning",
4345 "title": null
4346 }),
4347 json!({
4348 "sessionUpdate": "notice",
4349 "title": "MCP server unavailable"
4350 }),
4351 json!({
4352 "sessionUpdate": "notice",
4353 "severity": null,
4354 "title": "MCP server unavailable"
4355 }),
4356 ] {
4357 assert!(serde_json::from_str::<SessionUpdate>(&malformed.to_string()).is_err());
4358 assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
4359 }
4360 }
4361
4362 #[test]
4363 fn notice_tolerates_invalid_optional_display_fields_and_custom_severity() {
4364 use serde_json::json;
4365
4366 let wire = json!({
4367 "sessionUpdate": "notice",
4368 "severity": "_custom",
4369 "title": "Provider degraded",
4370 "description": 42,
4371 "_meta": false
4372 });
4373 let SessionUpdate::Notice(notice) = serde_json::from_value(wire.clone()).unwrap() else {
4374 panic!("expected notice");
4375 };
4376 assert_eq!(
4377 serde_json::from_str::<SessionUpdate>(&wire.to_string()).unwrap(),
4378 SessionUpdate::Notice(notice.clone())
4379 );
4380 assert_eq!(notice.severity, NoticeSeverity::Other("_custom".into()));
4381 assert_eq!(notice.description, None);
4382 assert_eq!(notice.meta, None);
4383 assert_eq!(
4384 serde_json::to_value(notice).unwrap(),
4385 json!({"severity": "_custom", "title": "Provider degraded"})
4386 );
4387 }
4388
4389 #[cfg(feature = "schemars")]
4390 #[test]
4391 fn notice_schema_preserves_required_fields_and_excludes_unknown_fallback() {
4392 use serde_json::json;
4393
4394 let schema = serde_json::to_value(schemars::schema_for!(Notice)).unwrap();
4395 assert_eq!(schema["required"], json!(["severity", "title"]));
4396 assert_eq!(schema["properties"]["title"]["minLength"], 1);
4397 for field in ["description", "_meta"] {
4398 assert!(
4399 schema["properties"][field]["type"]
4400 .as_array()
4401 .unwrap()
4402 .contains(&json!("null"))
4403 );
4404 }
4405 let fallback = serde_json::to_value(schemars::schema_for!(OtherSessionUpdate)).unwrap();
4406 assert!(
4407 fallback["not"]["anyOf"]
4408 .as_array()
4409 .unwrap()
4410 .iter()
4411 .any(|variant| variant["properties"]["sessionUpdate"]["const"] == "notice")
4412 );
4413 }
4414
4415 #[test]
4416 fn compaction_updates_preserve_patch_and_open_status_semantics() {
4417 use serde_json::json;
4418
4419 assert_eq!(
4420 serde_json::to_value(SessionUpdate::CompactionUpdate(
4421 CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
4422 ContentBlock::Text(crate::v2::TextContent::new("retained")),
4423 ]),
4424 ))
4425 .unwrap(),
4426 json!({
4427 "sessionUpdate": "compaction_update",
4428 "compactionId": "cmp_001",
4429 "status": "completed",
4430 "summary": [{ "type": "text", "text": "retained" }]
4431 })
4432 );
4433
4434 let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
4435 "sessionUpdate": "compaction_update",
4436 "compactionId": "cmp_001",
4437 "status": "paused",
4438 "summary": null
4439 }))
4440 .unwrap() else {
4441 panic!("expected compaction update");
4442 };
4443 assert_eq!(update.status, CompactionStatus::Other("paused".into()));
4444 assert!(update.summary.is_null());
4445 assert!(update.error.is_undefined());
4446 }
4447
4448 #[test]
4449 fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
4450 use serde_json::json;
4451
4452 assert!(
4453 serde_json::from_value::<SessionUpdate>(json!({
4454 "sessionUpdate": "compaction_update",
4455 "status": "completed"
4456 }))
4457 .is_err()
4458 );
4459 assert!(
4460 serde_json::from_value::<SessionUpdate>(json!({
4461 "sessionUpdate": "compaction_summary_chunk",
4462 "compactionId": "cmp_001"
4463 }))
4464 .is_err()
4465 );
4466 }
4467
4468 #[test]
4469 fn test_elicitation_capability_semantics() {
4470 use serde_json::json;
4471
4472 let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
4473 assert!(unsupported.elicitation.is_none());
4474
4475 let null: ClientCapabilities =
4476 serde_json::from_value(json!({ "elicitation": null })).unwrap();
4477 assert!(null.elicitation.is_none());
4478
4479 let malformed: ClientCapabilities =
4480 serde_json::from_value(json!({ "elicitation": false })).unwrap();
4481 assert!(malformed.elicitation.is_none());
4482
4483 let empty: ClientCapabilities =
4484 serde_json::from_value(json!({ "elicitation": {} })).unwrap();
4485 let empty = empty.elicitation.expect("present capability");
4486 assert!(!empty.supports_form());
4487 assert!(!empty.supports_url());
4488
4489 let form_only: ClientCapabilities = serde_json::from_value(json!({
4490 "elicitation": { "form": {} }
4491 }))
4492 .unwrap();
4493 let form_only = form_only.elicitation.expect("advertised capability");
4494 assert!(form_only.supports_form());
4495 assert!(!form_only.supports_url());
4496
4497 let url_only: ClientCapabilities = serde_json::from_value(json!({
4498 "elicitation": { "url": {} }
4499 }))
4500 .unwrap();
4501 let url_only = url_only.elicitation.expect("advertised capability");
4502 assert!(!url_only.supports_form());
4503 assert!(url_only.supports_url());
4504
4505 let both: ClientCapabilities = serde_json::from_value(json!({
4506 "elicitation": { "form": {}, "url": {} }
4507 }))
4508 .unwrap();
4509 let both = both.elicitation.expect("advertised capability");
4510 assert!(both.supports_form());
4511 assert!(both.supports_url());
4512 }
4513
4514 #[test]
4515 fn test_elicitation_method_routing_and_envelopes() {
4516 use serde_json::json;
4517
4518 assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
4519 assert_eq!(
4520 CLIENT_METHOD_NAMES.elicitation_complete,
4521 "elicitation/complete"
4522 );
4523
4524 let request =
4525 AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
4526 crate::v2::ElicitationFormMode::new(
4527 crate::v2::ElicitationSessionScope::new("sess_1"),
4528 crate::v2::ElicitationSchema::new(),
4529 ),
4530 "Choose a value",
4531 )));
4532 assert_eq!(request.method(), "elicitation/create");
4533 let method = Arc::from(request.method());
4534 let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
4535 id: crate::v2::RequestId::Number(7),
4536 method,
4537 params: Some(request),
4538 });
4539 assert_eq!(
4540 serde_json::to_value(request).unwrap(),
4541 json!({
4542 "jsonrpc": "2.0",
4543 "id": 7,
4544 "method": "elicitation/create",
4545 "params": {
4546 "mode": "form",
4547 "sessionId": "sess_1",
4548 "message": "Choose a value",
4549 "requestedSchema": { "type": "object", "properties": {} }
4550 }
4551 })
4552 );
4553
4554 let notification = AgentNotification::CompleteElicitationNotification(Box::new(
4555 CompleteElicitationNotification::new("elic_1"),
4556 ));
4557 assert_eq!(notification.method(), "elicitation/complete");
4558 let method = Arc::from(notification.method());
4559 let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
4560 method,
4561 params: Some(notification),
4562 });
4563 assert_eq!(
4564 serde_json::to_value(notification).unwrap(),
4565 json!({
4566 "jsonrpc": "2.0",
4567 "method": "elicitation/complete",
4568 "params": { "elicitationId": "elic_1" }
4569 })
4570 );
4571 }
4572
4573 #[test]
4574 fn test_client_capabilities_auth_defaults_on_malformed_value() {
4575 use serde_json::json;
4576
4577 let capabilities: ClientCapabilities = serde_json::from_value(json!({
4578 "auth": false
4579 }))
4580 .unwrap();
4581
4582 assert_eq!(capabilities.auth, None);
4583 }
4584
4585 #[test]
4586 fn test_serialization_behavior() {
4587 use serde_json::json;
4588
4589 assert_eq!(
4590 serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
4591 SessionInfoUpdate {
4592 title: MaybeUndefined::Undefined,
4593 updated_at: MaybeUndefined::Undefined,
4594 meta: MaybeUndefined::Undefined
4595 }
4596 );
4597 assert_eq!(
4598 serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
4599 .unwrap(),
4600 SessionInfoUpdate {
4601 title: MaybeUndefined::Null,
4602 updated_at: MaybeUndefined::Null,
4603 meta: MaybeUndefined::Undefined
4604 }
4605 );
4606 assert_eq!(
4607 serde_json::from_value::<SessionInfoUpdate>(
4608 json!({"title": "title", "updatedAt": "timestamp"})
4609 )
4610 .unwrap(),
4611 SessionInfoUpdate {
4612 title: MaybeUndefined::Value("title".to_string()),
4613 updated_at: MaybeUndefined::Value("timestamp".to_string()),
4614 meta: MaybeUndefined::Undefined
4615 }
4616 );
4617
4618 let clear_meta =
4619 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
4620 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4621
4622 let mut meta = Meta::new();
4623 meta.insert("source".to_string(), json!("session-info"));
4624
4625 assert_eq!(
4626 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
4627 "source": "session-info"
4628 }}))
4629 .unwrap()
4630 .meta,
4631 MaybeUndefined::Value(meta.clone())
4632 );
4633
4634 assert_eq!(
4635 serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
4636 json!({})
4637 );
4638
4639 assert_eq!(
4640 serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
4641 json!({"_meta": null})
4642 );
4643
4644 assert_eq!(
4645 serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
4646 json!({"_meta": {
4647 "source": "session-info"
4648 }})
4649 );
4650 assert_eq!(
4651 serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
4652 json!({"title": "title"})
4653 );
4654 assert_eq!(
4655 serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
4656 json!({"title": null})
4657 );
4658 assert_eq!(
4659 serde_json::to_value(
4660 SessionInfoUpdate::new()
4661 .title("title")
4662 .title(MaybeUndefined::Undefined)
4663 )
4664 .unwrap(),
4665 json!({})
4666 );
4667 }
4668
4669 #[test]
4670 fn test_content_chunk_message_id_serialization() {
4671 use serde_json::json;
4672
4673 assert_eq!(
4674 serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
4675 ContentBlock::Text(crate::v2::TextContent::new("Hello")),
4676 "msg_agent_c42b9",
4677 )))
4678 .unwrap(),
4679 json!({
4680 "sessionUpdate": "agent_message_chunk",
4681 "messageId": "msg_agent_c42b9",
4682 "content": {
4683 "type": "text",
4684 "text": "Hello"
4685 }
4686 })
4687 );
4688
4689 let err = serde_json::from_value::<ContentChunk>(json!({
4690 "content": {
4691 "type": "text",
4692 "text": "Hello"
4693 }
4694 }))
4695 .unwrap_err();
4696
4697 assert!(err.to_string().contains("messageId"), "{err}");
4698 }
4699
4700 #[test]
4701 fn test_tool_call_content_chunk_serialization() {
4702 use serde_json::json;
4703
4704 assert_eq!(
4705 serde_json::to_value(SessionUpdate::ToolCallContentChunk(
4706 ToolCallContentChunk::new(
4707 "call_001",
4708 crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
4709 )
4710 ))
4711 .unwrap(),
4712 json!({
4713 "sessionUpdate": "tool_call_content_chunk",
4714 "toolCallId": "call_001",
4715 "content": {
4716 "type": "content",
4717 "content": {
4718 "type": "text",
4719 "text": "partial output"
4720 }
4721 }
4722 })
4723 );
4724
4725 let err = serde_json::from_value::<ToolCallContentChunk>(json!({
4726 "content": {
4727 "type": "content",
4728 "content": {
4729 "type": "text",
4730 "text": "partial output"
4731 }
4732 }
4733 }))
4734 .unwrap_err();
4735
4736 assert!(err.to_string().contains("toolCallId"), "{err}");
4737 }
4738
4739 #[test]
4740 fn test_full_message_serialization() {
4741 use serde_json::json;
4742
4743 assert_eq!(
4744 serde_json::to_value(SessionUpdate::UserMessage(
4745 UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
4746 crate::v2::TextContent::new("Hello")
4747 )])
4748 ))
4749 .unwrap(),
4750 json!({
4751 "sessionUpdate": "user_message",
4752 "messageId": "msg_user_8f7a1",
4753 "content": [
4754 {
4755 "type": "text",
4756 "text": "Hello"
4757 }
4758 ]
4759 })
4760 );
4761
4762 assert_eq!(
4763 serde_json::to_value(SessionUpdate::AgentMessage(
4764 AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
4765 crate::v2::TextContent::new("Hello")
4766 )])
4767 ))
4768 .unwrap(),
4769 json!({
4770 "sessionUpdate": "agent_message",
4771 "messageId": "msg_agent_c42b9",
4772 "content": [
4773 {
4774 "type": "text",
4775 "text": "Hello"
4776 }
4777 ]
4778 })
4779 );
4780
4781 assert_eq!(
4782 serde_json::to_value(SessionUpdate::AgentThought(
4783 AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
4784 crate::v2::TextContent::new("Need to inspect the call sites first.")
4785 )])
4786 ))
4787 .unwrap(),
4788 json!({
4789 "sessionUpdate": "agent_thought",
4790 "messageId": "msg_thought_a12",
4791 "content": [
4792 {
4793 "type": "text",
4794 "text": "Need to inspect the call sites first."
4795 }
4796 ]
4797 })
4798 );
4799 }
4800
4801 #[test]
4802 fn test_message_upsert_serialization() {
4803 use serde_json::json;
4804
4805 assert_eq!(
4806 serde_json::to_value(SessionUpdate::UserMessage(
4807 UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
4808 ))
4809 .unwrap(),
4810 json!({
4811 "sessionUpdate": "user_message",
4812 "messageId": "msg_empty",
4813 "content": []
4814 })
4815 );
4816
4817 let empty = serde_json::from_value::<UserMessage>(json!({
4818 "messageId": "msg_empty",
4819 "content": []
4820 }))
4821 .unwrap();
4822 assert!(matches!(
4823 empty.content,
4824 MaybeUndefined::Value(ref content) if content.is_empty()
4825 ));
4826
4827 let patch = serde_json::from_value::<AgentMessage>(json!({
4828 "messageId": "msg_agent_c42b9"
4829 }))
4830 .unwrap();
4831 assert_eq!(patch.content, MaybeUndefined::Undefined);
4832 assert_eq!(patch.meta, MaybeUndefined::Undefined);
4833
4834 let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
4835 "messageId": "msg_agent_c42b9",
4836 "_meta": false
4837 }))
4838 .unwrap();
4839 assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
4840
4841 let patch = serde_json::from_value::<AgentThought>(json!({
4842 "messageId": "msg_thought_a12"
4843 }))
4844 .unwrap();
4845 assert_eq!(patch.content, MaybeUndefined::Undefined);
4846
4847 let clear = serde_json::from_value::<UserMessage>(json!({
4848 "messageId": "msg_user_8f7a1",
4849 "content": null
4850 }))
4851 .unwrap();
4852 assert_eq!(clear.content, MaybeUndefined::Null);
4853
4854 let clear_meta = serde_json::from_value::<UserMessage>(json!({
4855 "messageId": "msg_user_8f7a1",
4856 "_meta": null
4857 }))
4858 .unwrap();
4859 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
4860
4861 let mut meta = Meta::new();
4862 meta.insert("source".to_string(), json!("replay"));
4863
4864 assert_eq!(
4865 serde_json::to_value(SessionUpdate::UserMessage(
4866 UserMessage::new("msg_user_8f7a1").meta(meta)
4867 ))
4868 .unwrap(),
4869 json!({
4870 "sessionUpdate": "user_message",
4871 "messageId": "msg_user_8f7a1",
4872 "_meta": {
4873 "source": "replay"
4874 }
4875 })
4876 );
4877
4878 assert_eq!(
4879 serde_json::to_value(SessionUpdate::UserMessage(
4880 UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
4881 ))
4882 .unwrap(),
4883 json!({
4884 "sessionUpdate": "user_message",
4885 "messageId": "msg_user_8f7a1",
4886 "_meta": null
4887 })
4888 );
4889 }
4890
4891 #[test]
4892 fn test_usage_update_serialization() {
4893 use serde_json::json;
4894
4895 assert_eq!(
4896 serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
4897 53_000, 200_000
4898 )))
4899 .unwrap(),
4900 json!({
4901 "sessionUpdate": "usage_update",
4902 "used": 53000,
4903 "size": 200_000
4904 })
4905 );
4906
4907 assert_eq!(
4908 serde_json::to_value(SessionUpdate::UsageUpdate(
4909 UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
4910 ))
4911 .unwrap(),
4912 json!({
4913 "sessionUpdate": "usage_update",
4914 "used": 53000,
4915 "size": 200_000,
4916 "cost": {
4917 "amount": 0.045,
4918 "currency": "USD"
4919 }
4920 })
4921 );
4922
4923 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
4924 "sessionUpdate": "usage_update",
4925 "used": 53000,
4926 "size": 200_000,
4927 "cost": null
4928 }))
4929 .unwrap() else {
4930 panic!("expected usage update");
4931 };
4932
4933 assert_eq!(update.cost, MaybeUndefined::Null);
4934
4935 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
4936 "sessionUpdate": "usage_update",
4937 "used": 53000,
4938 "size": 200_000
4939 }))
4940 .unwrap() else {
4941 panic!("expected usage update");
4942 };
4943
4944 assert_eq!(update.cost, MaybeUndefined::Undefined);
4945 }
4946
4947 #[test]
4948 fn error_stop_reason_carries_failure_beside_discriminator() {
4949 use crate::v2::{Error, ErrorStopReason};
4950 use serde_json::json;
4951
4952 let update =
4953 SessionUpdate::StateUpdate(StateUpdate::Idle(IdleStateUpdate::new().stop_reason(
4954 StopReason::from(ErrorStopReason::new().error(Error::auth_required())),
4955 )));
4956 let wire = json!({
4957 "sessionUpdate": "state_update",
4958 "state": "idle",
4959 "stopReason": "error",
4960 "error": { "code": -32000, "message": "Authentication required" }
4961 });
4962 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4963 assert_eq!(
4964 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4965 update
4966 );
4967
4968 let with_data =
4969 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::from(
4970 ErrorStopReason::new().error(
4971 Error::new(-32099, "Provider unavailable").data(json!({ "status": 503 })),
4972 ),
4973 )));
4974 let wire = json!({
4975 "state": "idle",
4976 "stopReason": "error",
4977 "error": {
4978 "code": -32099,
4979 "message": "Provider unavailable",
4980 "data": { "status": 503 }
4981 }
4982 });
4983 assert_eq!(serde_json::to_value(&with_data).unwrap(), wire);
4984 assert_eq!(
4985 serde_json::from_value::<StateUpdate>(wire).unwrap(),
4986 with_data
4987 );
4988
4989 let without_details = StateUpdate::Idle(
4992 IdleStateUpdate::new().stop_reason(StopReason::from(ErrorStopReason::new())),
4993 );
4994 for error in [
4995 None,
4996 Some(json!(null)),
4997 Some(json!("failed")),
4998 Some(json!({ "message": "missing code" })),
4999 ] {
5000 let mut wire = json!({ "state": "idle", "stopReason": "error" });
5001 if let Some(error) = error {
5002 wire["error"] = error;
5003 }
5004 assert_eq!(
5005 serde_json::from_value::<StateUpdate>(wire).unwrap(),
5006 without_details
5007 );
5008 }
5009 assert_eq!(
5010 serde_json::to_value(&without_details).unwrap(),
5011 json!({ "state": "idle", "stopReason": "error" })
5012 );
5013
5014 let parsed: StateUpdate = serde_json::from_value(json!({
5018 "state": "idle",
5019 "stopReason": "end_turn",
5020 "error": { "code": -32603, "message": "Internal error" }
5021 }))
5022 .unwrap();
5023 assert_eq!(
5024 parsed,
5025 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn))
5026 );
5027 }
5028
5029 #[test]
5030 fn flattened_stop_reason_keeps_open_enum_tolerance() {
5031 use crate::v2::OtherStopReason;
5032 use serde_json::json;
5033
5034 let meta: Meta = [("source".into(), json!("test"))].into_iter().collect();
5035
5036 for stop_reason in [None, Some(json!(null)), Some(json!(true)), Some(json!({}))] {
5039 let mut wire = json!({ "state": "idle", "_meta": { "source": "test" } });
5040 if let Some(stop_reason) = stop_reason {
5041 wire["stopReason"] = stop_reason;
5042 }
5043 let StateUpdate::Idle(idle) = serde_json::from_value(wire).unwrap() else {
5044 panic!("expected idle state update");
5045 };
5046 assert_eq!(idle.stop_reason, None);
5047 assert_eq!(idle.meta, Some(meta.clone()));
5048 }
5049
5050 let wire = json!({
5053 "state": "idle",
5054 "stopReason": "_paused",
5055 "resumeAfter": 30,
5056 "_meta": { "source": "test" }
5057 });
5058 let parsed: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
5059 let expected = StateUpdate::Idle(
5060 IdleStateUpdate::new()
5061 .stop_reason(StopReason::Other(OtherStopReason::new(
5062 "_paused",
5063 [("resumeAfter".into(), json!(30))].into_iter().collect(),
5064 )))
5065 .meta(meta),
5066 );
5067 assert_eq!(parsed, expected);
5068 assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
5069 }
5070
5071 #[test]
5072 fn known_stop_reasons_are_never_read_as_other() {
5073 use crate::v2::{ErrorStopReason, OtherStopReason};
5074 use serde_json::json;
5075
5076 let known = [
5077 StopReason::EndTurn,
5078 StopReason::MaxTokens,
5079 StopReason::MaxTurnRequests,
5080 StopReason::Refusal,
5081 StopReason::Cancelled,
5082 StopReason::Error(ErrorStopReason::new()),
5083 ];
5084 for stop_reason in &known {
5086 match stop_reason {
5087 StopReason::EndTurn
5088 | StopReason::MaxTokens
5089 | StopReason::MaxTurnRequests
5090 | StopReason::Refusal
5091 | StopReason::Cancelled
5092 | StopReason::Error(_)
5093 | StopReason::Other(_) => {}
5094 }
5095 }
5096
5097 for stop_reason in known {
5098 let update = StateUpdate::Idle(IdleStateUpdate::new().stop_reason(stop_reason));
5099 let wire = serde_json::to_value(&update).unwrap();
5100 assert_eq!(
5101 serde_json::from_value::<StateUpdate>(wire.clone()).unwrap(),
5102 update
5103 );
5104 assert!(
5105 serde_json::from_value::<OtherStopReason>(
5106 json!({ "stopReason": wire["stopReason"] })
5107 )
5108 .is_err(),
5109 "{} must not be accepted as a custom stop reason",
5110 wire["stopReason"]
5111 );
5112 }
5113 }
5114
5115 #[test]
5116 fn unknown_stop_reason_leaves_usage_on_the_idle_update() {
5117 use serde_json::json;
5118
5119 let usage = json!({ "totalTokens": 10, "inputTokens": 6, "outputTokens": 4 });
5120 let wire = json!({ "state": "idle", "stopReason": "_paused", "usage": usage });
5121 let StateUpdate::Idle(idle) = serde_json::from_value(wire.clone()).unwrap() else {
5122 panic!("expected idle state update");
5123 };
5124 let Some(StopReason::Other(other)) = &idle.stop_reason else {
5125 panic!("expected a custom stop reason");
5126 };
5127 assert!(other.fields.is_empty());
5128
5129 #[cfg(feature = "unstable_end_turn_token_usage")]
5130 {
5131 assert!(idle.usage.is_some());
5132 assert_eq!(serde_json::to_value(&idle).unwrap()["usage"], usage);
5133 }
5134 #[cfg(not(feature = "unstable_end_turn_token_usage"))]
5137 assert!(serde_json::to_value(&idle).unwrap().get("usage").is_none());
5138 }
5139
5140 #[test]
5141 fn test_state_update_serialization() {
5142 use serde_json::json;
5143
5144 assert_eq!(
5145 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
5146 RunningStateUpdate::new()
5147 )))
5148 .unwrap(),
5149 json!({
5150 "sessionUpdate": "state_update",
5151 "state": "running"
5152 })
5153 );
5154
5155 assert_eq!(
5156 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
5157 IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
5158 )))
5159 .unwrap(),
5160 json!({
5161 "sessionUpdate": "state_update",
5162 "state": "idle",
5163 "stopReason": "end_turn"
5164 })
5165 );
5166
5167 let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
5168 "sessionUpdate": "state_update",
5169 "state": "requires_action"
5170 }))
5171 .unwrap() else {
5172 panic!("expected state update");
5173 };
5174
5175 assert!(matches!(update, StateUpdate::RequiresAction(_)));
5176
5177 let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
5178 "sessionUpdate": "state_update",
5179 "state": "idle",
5180 "stopReason": null
5181 }))
5182 .unwrap() else {
5183 panic!("expected idle state update");
5184 };
5185
5186 assert_eq!(update.stop_reason, None);
5187
5188 let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
5189 serde_json::from_value(json!({
5190 "sessionUpdate": "state_update",
5191 "state": "_paused",
5192 "label": "Paused"
5193 }))
5194 .unwrap()
5195 else {
5196 panic!("expected unknown state update");
5197 };
5198
5199 assert_eq!(update.state, "_paused");
5200 assert_eq!(update.fields["label"], json!("Paused"));
5201 }
5202
5203 #[test]
5204 fn session_update_preserves_unknown_variant() {
5205 use serde_json::json;
5206
5207 let update: SessionUpdate = serde_json::from_value(json!({
5208 "sessionUpdate": "_status_badge",
5209 "label": "Indexing",
5210 "progress": 0.5
5211 }))
5212 .unwrap();
5213
5214 let SessionUpdate::Other(unknown) = update else {
5215 panic!("expected unknown session update");
5216 };
5217
5218 assert_eq!(unknown.session_update, "_status_badge");
5219 assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
5220 assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
5221
5222 assert_eq!(
5223 serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
5224 json!({
5225 "sessionUpdate": "_status_badge",
5226 "label": "Indexing",
5227 "progress": 0.5
5228 })
5229 );
5230 }
5231
5232 #[test]
5233 fn terminal_session_updates_use_known_discriminators() {
5234 use serde_json::json;
5235
5236 assert_eq!(
5237 serde_json::to_value(SessionUpdate::TerminalUpdate(
5238 TerminalUpdate::new("term_1").command("cargo test")
5239 ))
5240 .unwrap(),
5241 json!({
5242 "sessionUpdate": "terminal_update",
5243 "terminalId": "term_1",
5244 "command": "cargo test"
5245 })
5246 );
5247 assert_eq!(
5248 serde_json::to_value(SessionUpdate::TerminalOutputChunk(
5249 TerminalOutputChunk::new("term_1", "dGVzdAo=")
5250 ))
5251 .unwrap(),
5252 json!({
5253 "sessionUpdate": "terminal_output_chunk",
5254 "terminalId": "term_1",
5255 "data": "dGVzdAo="
5256 })
5257 );
5258 }
5259
5260 #[test]
5261 fn session_update_does_not_hide_malformed_known_terminal_variants() {
5262 use serde_json::json;
5263
5264 assert!(
5265 serde_json::from_value::<SessionUpdate>(json!({
5266 "sessionUpdate": "terminal_update"
5267 }))
5268 .is_err()
5269 );
5270 assert!(
5271 serde_json::from_value::<SessionUpdate>(json!({
5272 "sessionUpdate": "terminal_output_chunk",
5273 "terminalId": "term_1"
5274 }))
5275 .is_err()
5276 );
5277 }
5278
5279 #[test]
5280 fn test_plan_update_serialization() {
5281 use serde_json::json;
5282
5283 let plan_update =
5284 SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
5285 "plan-1",
5286 vec![crate::v2::PlanEntry::new(
5287 "Step 1",
5288 crate::v2::PlanEntryPriority::High,
5289 crate::v2::PlanEntryStatus::Pending,
5290 )],
5291 )));
5292
5293 assert_eq!(
5294 serde_json::to_value(plan_update).unwrap(),
5295 json!({
5296 "sessionUpdate": "plan_update",
5297 "plan": {
5298 "type": "items",
5299 "planId": "plan-1",
5300 "entries": [
5301 {
5302 "content": "Step 1",
5303 "priority": "high",
5304 "status": "pending"
5305 }
5306 ]
5307 }
5308 })
5309 );
5310 }
5311
5312 #[cfg(feature = "unstable_plan_operations")]
5313 #[test]
5314 fn test_plan_removed_serialization() {
5315 use serde_json::json;
5316
5317 assert_eq!(
5318 serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
5319 json!({
5320 "sessionUpdate": "plan_removed",
5321 "planId": "plan-1"
5322 })
5323 );
5324 }
5325
5326 #[test]
5327 fn available_command_uses_type_discriminator() {
5328 use serde_json::json;
5329
5330 let command = AvailableCommand::Command(
5331 Command::new("web", "Search the web")
5332 .input(AvailableCommandInput::Text(TextCommandInput::new("Query"))),
5333 );
5334
5335 let json = serde_json::to_value(&command).unwrap();
5336 assert_eq!(
5337 json,
5338 json!({
5339 "type": "command",
5340 "name": "web",
5341 "description": "Search the web",
5342 "input": { "type": "text", "hint": "Query" }
5343 })
5344 );
5345
5346 let roundtripped: AvailableCommand = serde_json::from_value(json).unwrap();
5347 assert_eq!(roundtripped, command);
5348 assert_eq!(roundtripped.name(), "web");
5349 assert_eq!(roundtripped.description(), "Search the web");
5350 }
5351
5352 #[test]
5353 fn available_command_preserves_unknown_typed_variant() {
5354 use serde_json::json;
5355
5356 let payload = json!({
5357 "type": "_skill",
5358 "name": "pdf-processing",
5359 "description": "Extract text from PDFs",
5360 "uri": "skill://pdf-processing/SKILL.md",
5361 "_meta": { "example.com/source": "mcp" }
5362 });
5363 let command: AvailableCommand = serde_json::from_value(payload.clone()).unwrap();
5364
5365 assert_eq!(command.name(), "pdf-processing");
5366 assert_eq!(command.description(), "Extract text from PDFs");
5367 assert!(command.meta().is_some());
5368
5369 let AvailableCommand::Other(unknown) = &command else {
5370 panic!("expected unknown command");
5371 };
5372 assert_eq!(unknown.type_, "_skill");
5373 assert_eq!(
5374 unknown.fields.get("uri"),
5375 Some(&json!("skill://pdf-processing/SKILL.md"))
5376 );
5377 assert_eq!(serde_json::to_value(&command).unwrap(), payload);
5378 }
5379
5380 #[test]
5381 fn available_command_unknown_tolerates_invalid_meta() {
5382 use serde_json::json;
5383
5384 let command: AvailableCommand = serde_json::from_value(json!({
5385 "type": "_skill",
5386 "name": "pdf-processing",
5387 "description": "Extract text from PDFs",
5388 "_meta": "oops"
5389 }))
5390 .unwrap();
5391
5392 assert!(matches!(command, AvailableCommand::Other(_)));
5393 assert!(command.meta().is_none());
5394 }
5395
5396 #[test]
5397 fn available_command_unknown_does_not_hide_malformed_command_variant() {
5398 use serde_json::json;
5399
5400 assert!(
5402 serde_json::from_value::<AvailableCommand>(json!({
5403 "name": "test",
5404 "description": "Run project tests"
5405 }))
5406 .is_err()
5407 );
5408 assert!(
5409 serde_json::from_value::<AvailableCommand>(json!({
5410 "type": "command",
5411 "name": "test"
5412 }))
5413 .is_err()
5414 );
5415 assert!(
5416 serde_json::from_value::<OtherAvailableCommand>(json!({
5417 "type": "command",
5418 "name": "test",
5419 "description": "Run project tests"
5420 }))
5421 .is_err()
5422 );
5423 assert!(
5424 serde_json::from_value::<AvailableCommand>(json!({
5425 "type": "_skill",
5426 "name": "pdf-processing"
5427 }))
5428 .is_err()
5429 );
5430 }
5431
5432 #[test]
5433 fn available_command_input_preserves_unknown_typed_variant() {
5434 use serde_json::json;
5435
5436 let input: AvailableCommandInput = serde_json::from_value(json!({
5437 "type": "_choices",
5438 "hint": "Pick one",
5439 "options": ["fast", "careful"]
5440 }))
5441 .unwrap();
5442
5443 let AvailableCommandInput::Other(unknown) = input else {
5444 panic!("expected unknown command input");
5445 };
5446
5447 assert_eq!(unknown.type_, "_choices");
5448 assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
5449 assert_eq!(
5450 unknown.fields.get("options"),
5451 Some(&json!(["fast", "careful"]))
5452 );
5453 assert_eq!(
5454 serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
5455 json!({
5456 "type": "_choices",
5457 "hint": "Pick one",
5458 "options": ["fast", "careful"]
5459 })
5460 );
5461 }
5462
5463 #[test]
5464 fn available_command_input_text_uses_type_discriminator() {
5465 use serde_json::json;
5466
5467 let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
5468
5469 let json = serde_json::to_value(&input).unwrap();
5470 assert_eq!(
5471 json,
5472 json!({
5473 "type": "text",
5474 "hint": "Describe changes"
5475 })
5476 );
5477
5478 let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
5479 assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
5480 }
5481
5482 #[test]
5483 fn request_permission_subject_tool_call_uses_type_discriminator() {
5484 use serde_json::json;
5485
5486 let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
5487
5488 let json = serde_json::to_value(&subject).unwrap();
5489 assert_eq!(
5490 json,
5491 json!({
5492 "type": "tool_call",
5493 "toolCall": {
5494 "toolCallId": "call_001"
5495 }
5496 })
5497 );
5498
5499 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5500 assert!(matches!(
5501 roundtripped,
5502 RequestPermissionSubject::ToolCall(_)
5503 ));
5504 }
5505
5506 #[test]
5507 fn request_permission_subject_command_uses_type_discriminator() {
5508 use serde_json::json;
5509
5510 let mut meta = Meta::new();
5511 meta.insert("source".to_string(), json!("shell"));
5512 let subject = RequestPermissionSubject::from(
5513 CommandPermissionSubject::new("cargo test", "/workspace/project")
5514 .tool_call_id("call_001")
5515 .terminal_id("term_1")
5516 .meta(meta),
5517 );
5518
5519 let json = serde_json::to_value(&subject).unwrap();
5520 assert_eq!(
5521 json,
5522 json!({
5523 "type": "command",
5524 "command": "cargo test",
5525 "cwd": "/workspace/project",
5526 "toolCallId": "call_001",
5527 "terminalId": "term_1",
5528 "_meta": {
5529 "source": "shell"
5530 }
5531 })
5532 );
5533
5534 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5535 assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
5536 }
5537
5538 #[test]
5539 fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
5540 use serde_json::json;
5541
5542 let subject: RequestPermissionSubject = serde_json::from_value(json!({
5543 "type": "command",
5544 "command": "cargo test",
5545 "cwd": "/workspace/project",
5546 "toolCallId": null,
5547 "terminalId": null,
5548 "_meta": null
5549 }))
5550 .unwrap();
5551
5552 let RequestPermissionSubject::Command(subject) = subject else {
5553 panic!("expected command permission subject");
5554 };
5555 assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
5556 assert_eq!(subject.tool_call_id, None);
5557 assert_eq!(subject.terminal_id, None);
5558 assert_eq!(subject.meta, None);
5559 assert_eq!(
5560 serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
5561 json!({
5562 "type": "command",
5563 "command": "cargo test",
5564 "cwd": "/workspace/project"
5565 })
5566 );
5567 }
5568
5569 #[test]
5570 fn request_permission_subject_preserves_unknown_variant() {
5571 use serde_json::json;
5572
5573 let subject: RequestPermissionSubject = serde_json::from_value(json!({
5574 "type": "_review",
5575 "reason": "needs-review",
5576 "retryAfterSeconds": 30
5577 }))
5578 .unwrap();
5579
5580 let RequestPermissionSubject::Other(unknown) = subject else {
5581 panic!("expected unknown permission subject");
5582 };
5583
5584 assert_eq!(unknown.type_, "_review");
5585 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
5586 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
5587 assert_eq!(
5588 serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
5589 json!({
5590 "type": "_review",
5591 "reason": "needs-review",
5592 "retryAfterSeconds": 30
5593 })
5594 );
5595 }
5596
5597 #[test]
5598 fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
5599 use serde_json::json;
5600
5601 assert!(
5602 serde_json::from_value::<RequestPermissionSubject>(json!({
5603 "type": "tool_call"
5604 }))
5605 .is_err()
5606 );
5607 assert!(
5608 serde_json::from_value::<RequestPermissionSubject>(json!({
5609 "type": 1
5610 }))
5611 .is_err()
5612 );
5613 assert!(
5614 serde_json::from_value::<RequestPermissionSubject>(json!({
5615 "type": "command",
5616 "cwd": "/workspace/project"
5617 }))
5618 .is_err()
5619 );
5620 assert!(
5621 serde_json::from_value::<RequestPermissionSubject>(json!({
5622 "type": "command",
5623 "command": "cargo test"
5624 }))
5625 .is_err()
5626 );
5627 assert!(
5628 serde_json::from_value::<RequestPermissionSubject>(json!({
5629 "type": "command",
5630 "command": "cargo test",
5631 "cwd": null
5632 }))
5633 .is_err()
5634 );
5635 }
5636
5637 #[test]
5638 fn request_permission_title_and_description_are_separate_from_tool_call_content() {
5639 use serde_json::json;
5640
5641 let request =
5642 RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
5643 .description("Allow this tool to edit src/main.rs?")
5644 .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
5645 "call_001",
5646 )));
5647
5648 assert_eq!(
5649 serde_json::to_value(request).unwrap(),
5650 json!({
5651 "sessionId": "sess_abc123def456",
5652 "title": "Approve file edit?",
5653 "description": "Allow this tool to edit src/main.rs?",
5654 "subject": {
5655 "type": "tool_call",
5656 "toolCall": {
5657 "toolCallId": "call_001"
5658 }
5659 },
5660 "options": []
5661 })
5662 );
5663 }
5664
5665 #[test]
5666 fn request_permission_requires_title_and_allows_missing_subject() {
5667 use serde_json::json;
5668
5669 let request = RequestPermissionRequest::new(
5670 "sess_abc123def456",
5671 "Approve elevated permissions?",
5672 Vec::new(),
5673 );
5674
5675 assert_eq!(
5676 serde_json::to_value(request).unwrap(),
5677 json!({
5678 "sessionId": "sess_abc123def456",
5679 "title": "Approve elevated permissions?",
5680 "options": []
5681 })
5682 );
5683
5684 let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
5685 "sessionId": "sess_abc123def456",
5686 "title": "Approve elevated permissions?",
5687 "options": []
5688 }))
5689 .unwrap();
5690 assert!(missing_subject.subject.is_none());
5691
5692 let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
5693 "sessionId": "sess_abc123def456",
5694 "title": "Approve elevated permissions?",
5695 "subject": null,
5696 "options": []
5697 }))
5698 .unwrap();
5699 assert!(null_subject.subject.is_none());
5700
5701 assert!(
5702 serde_json::from_value::<RequestPermissionRequest>(json!({
5703 "sessionId": "sess_abc123def456",
5704 "options": []
5705 }))
5706 .is_err()
5707 );
5708 }
5709
5710 #[test]
5711 fn request_permission_outcome_preserves_unknown_variant() {
5712 use serde_json::json;
5713
5714 let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
5715 "outcome": "_defer",
5716 "reason": "needs-review",
5717 "retryAfterSeconds": 30
5718 }))
5719 .unwrap();
5720
5721 let RequestPermissionOutcome::Other(unknown) = outcome else {
5722 panic!("expected unknown permission outcome");
5723 };
5724
5725 assert_eq!(unknown.outcome, "_defer");
5726 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
5727 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
5728 assert_eq!(
5729 serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
5730 json!({
5731 "outcome": "_defer",
5732 "reason": "needs-review",
5733 "retryAfterSeconds": 30
5734 })
5735 );
5736 }
5737
5738 #[test]
5739 fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
5740 use serde_json::json;
5741
5742 assert!(
5743 serde_json::from_value::<RequestPermissionOutcome>(json!({
5744 "outcome": "selected"
5745 }))
5746 .is_err()
5747 );
5748 assert!(
5749 serde_json::from_value::<RequestPermissionOutcome>(json!({
5750 "outcome": 1
5751 }))
5752 .is_err()
5753 );
5754 }
5755
5756 #[test]
5757 fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
5758 use serde_json::json;
5759
5760 assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
5761 assert!(
5762 serde_json::from_value::<AvailableCommandInput>(json!({
5763 "hint": "Pick one"
5764 }))
5765 .is_err()
5766 );
5767 assert!(
5768 serde_json::from_value::<AvailableCommandInput>(json!({
5769 "type": 1,
5770 "hint": "Pick one"
5771 }))
5772 .is_err()
5773 );
5774 assert!(
5775 serde_json::from_value::<OtherAvailableCommandInput>(json!({
5776 "type": "text",
5777 "hint": "Pick one"
5778 }))
5779 .is_err()
5780 );
5781 }
5782
5783 #[cfg(feature = "unstable_mcp_over_acp")]
5784 #[test]
5785 fn test_agent_mcp_request_method_names() {
5786 use serde_json::json;
5787
5788 let params: serde_json::Map<String, serde_json::Value> =
5789 [("cursor".to_string(), json!("abc"))].into_iter().collect();
5790
5791 assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
5792 assert_eq!(
5793 AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
5794 "server-1",
5795 "req-1",
5796 "tools/list"
5797 )))
5798 .method(),
5799 "mcp/message"
5800 );
5801 assert_eq!(
5802 serde_json::to_value(
5803 MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
5804 )
5805 .unwrap(),
5806 json!({
5807 "serverId": "server-1",
5808 "requestId": "req-1",
5809 "method": "tools/list",
5810 "params": { "cursor": "abc" }
5811 })
5812 );
5813
5814 let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
5815 "serverId": "server-1",
5816 "requestId": "req-1",
5817 "method": "tools/list",
5818 "params": null,
5819 "_meta": null
5820 }))
5821 .unwrap();
5822 assert_eq!(request_with_null_params.params, None);
5823 assert_eq!(request_with_null_params.meta, None);
5824 for key in ["serverId", "requestId", "method"] {
5825 let mut value =
5826 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5827 value.as_object_mut().unwrap().remove(key);
5828 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5829 }
5830 for key in ["serverId", "requestId", "method"] {
5831 let mut value =
5832 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
5833 value[key] = serde_json::Value::Null;
5834 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
5835 }
5836 }
5837
5838 #[test]
5839 fn test_auth_capabilities_serialize_terminal_support_as_object() {
5840 use serde_json::json;
5841
5842 let capabilities = ClientAuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
5843
5844 assert_eq!(
5845 serde_json::to_value(&capabilities).unwrap(),
5846 json!({
5847 "terminal": {}
5848 })
5849 );
5850
5851 let deserialized: ClientAuthCapabilities = serde_json::from_value(json!({
5852 "terminal": false
5853 }))
5854 .unwrap();
5855 assert!(deserialized.terminal.is_none());
5856 }
5857
5858 #[test]
5859 fn request_permission_request_rejects_malformed_options() {
5860 use serde_json::json;
5861
5862 assert!(
5863 serde_json::from_value::<RequestPermissionRequest>(json!({
5864 "sessionId": "sess-1",
5865 "title": "Run tool?",
5866 "options": "not-an-array"
5867 }))
5868 .is_err()
5869 );
5870 assert!(
5871 serde_json::from_value::<RequestPermissionRequest>(json!({
5872 "sessionId": "sess-1",
5873 "title": "Run tool?",
5874 "options": [{"optionId": "allow"}]
5875 }))
5876 .is_err()
5877 );
5878 }
5879
5880 #[cfg(feature = "unstable_plan_operations")]
5881 #[test]
5882 fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
5883 use serde_json::json;
5884
5885 assert!(
5886 serde_json::from_value::<SessionUpdate>(json!({
5887 "sessionUpdate": "plan_removed"
5888 }))
5889 .is_err()
5890 );
5891 }
5892}