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, SkipListener};
28
29#[cfg(feature = "unstable_mcp_over_acp")]
30use super::mcp::{
31 ConnectMcpRequest, ConnectMcpResponse, DisconnectMcpRequest, DisconnectMcpResponse,
32 MCP_CONNECT_METHOD_NAME, MCP_DISCONNECT_METHOD_NAME, MCP_MESSAGE_METHOD_NAME,
33 MessageMcpNotification, MessageMcpRequest, MessageMcpResponse,
34};
35
36#[cfg(feature = "unstable_nes")]
37use super::{ClientNesCapabilities, PositionEncodingKind};
38
39#[serde_as]
47#[skip_serializing_none]
48#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION)))]
51#[serde(rename_all = "camelCase")]
52#[non_exhaustive]
53pub struct UpdateSessionNotification {
54 pub session_id: SessionId,
56 pub update: SessionUpdate,
58 #[serde_as(deserialize_as = "DefaultOnError")]
64 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
65 #[serde(default)]
66 #[serde(rename = "_meta")]
67 pub meta: Option<Meta>,
68}
69
70impl UpdateSessionNotification {
71 #[must_use]
73 pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
74 Self {
75 session_id: session_id.into(),
76 update,
77 meta: None,
78 }
79 }
80
81 #[must_use]
87 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
88 self.meta = meta.into_option();
89 self
90 }
91}
92
93#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
100#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
101#[non_exhaustive]
102pub enum SessionUpdate {
103 UserMessageChunk(ContentChunk),
105 UserMessage(UserMessage),
111 AgentMessageChunk(ContentChunk),
113 AgentMessage(AgentMessage),
119 AgentThoughtChunk(ContentChunk),
121 AgentThought(AgentThought),
127 StateUpdate(StateUpdate),
129 ToolCallContentChunk(ToolCallContentChunk),
131 ToolCallUpdate(ToolCallUpdate),
133 TerminalUpdate(TerminalUpdate),
135 TerminalOutputChunk(TerminalOutputChunk),
137 PlanUpdate(PlanUpdate),
140 #[cfg(feature = "unstable_plan_operations")]
146 PlanRemoved(PlanRemoved),
147 AvailableCommandsUpdate(AvailableCommandsUpdate),
149 ConfigOptionUpdate(ConfigOptionUpdate),
151 SessionInfoUpdate(SessionInfoUpdate),
153 UsageUpdate(UsageUpdate),
155 #[cfg(feature = "unstable_session_notices")]
164 Notice(Notice),
165 #[cfg(feature = "unstable_session_compaction")]
171 CompactionUpdate(CompactionUpdate),
172 #[cfg(feature = "unstable_session_compaction")]
178 CompactionSummaryChunk(CompactionSummaryChunk),
179 #[serde(untagged)]
189 Other(OtherSessionUpdate),
190}
191
192#[cfg(feature = "unstable_session_notices")]
198#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200#[serde(rename_all = "snake_case")]
201#[non_exhaustive]
202pub enum NoticeSeverity {
203 Info,
205 Warning,
207 Error,
209 #[serde(untagged)]
214 Other(String),
215}
216
217#[cfg(feature = "unstable_session_notices")]
229#[serde_as]
230#[skip_serializing_none]
231#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
233#[serde(rename_all = "camelCase")]
234#[non_exhaustive]
235pub struct Notice {
236 pub severity: NoticeSeverity,
238 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
240 pub title: String,
241 #[serde_as(deserialize_as = "DefaultOnError")]
245 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
246 #[serde(default)]
247 pub description: Option<String>,
248 #[serde_as(deserialize_as = "DefaultOnError")]
252 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
253 #[serde(default, rename = "_meta")]
254 pub meta: Option<Meta>,
255}
256
257#[cfg(feature = "unstable_session_notices")]
258impl Notice {
259 #[must_use]
261 pub fn new(severity: NoticeSeverity, title: impl Into<String>) -> Self {
262 Self {
263 severity,
264 title: title.into(),
265 description: None,
266 meta: None,
267 }
268 }
269
270 #[must_use]
272 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
273 self.description = description.into_option();
274 self
275 }
276
277 #[must_use]
279 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
280 self.meta = meta.into_option();
281 self
282 }
283}
284
285#[cfg(feature = "unstable_session_compaction")]
291#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
293#[serde(transparent)]
294#[from(forward)]
295#[non_exhaustive]
296pub struct CompactionId(pub Arc<str>);
297
298#[cfg(feature = "unstable_session_compaction")]
299impl CompactionId {
300 #[must_use]
302 pub fn new(id: impl Into<Self>) -> Self {
303 id.into()
304 }
305}
306
307#[cfg(feature = "unstable_session_compaction")]
313#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
315#[serde(rename_all = "snake_case")]
316#[non_exhaustive]
317pub enum CompactionStatus {
318 InProgress,
320 Completed,
322 Failed,
324 Cancelled,
326 #[serde(untagged)]
331 Other(String),
332}
333
334#[cfg(feature = "unstable_session_compaction")]
346#[serde_as]
347#[skip_serializing_none]
348#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
350#[serde(rename_all = "camelCase")]
351#[non_exhaustive]
352pub struct CompactionUpdate {
353 pub compaction_id: CompactionId,
355 pub status: CompactionStatus,
357 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
359 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
360 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
361 pub summary: MaybeUndefined<Vec<ContentBlock>>,
362 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
364 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
365 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
366 pub error: MaybeUndefined<String>,
367 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
369 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
370 #[serde(
371 rename = "_meta",
372 default,
373 skip_serializing_if = "MaybeUndefined::is_undefined"
374 )]
375 pub meta: MaybeUndefined<Meta>,
376}
377
378#[cfg(feature = "unstable_session_compaction")]
379impl CompactionUpdate {
380 #[must_use]
382 pub fn new(compaction_id: impl Into<CompactionId>, status: CompactionStatus) -> Self {
383 Self {
384 compaction_id: compaction_id.into(),
385 status,
386 summary: MaybeUndefined::Undefined,
387 error: MaybeUndefined::Undefined,
388 meta: MaybeUndefined::Undefined,
389 }
390 }
391
392 #[must_use]
394 pub fn summary(mut self, summary: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
395 self.summary = summary.into_maybe_undefined();
396 self
397 }
398
399 #[must_use]
401 pub fn error(mut self, error: impl IntoMaybeUndefined<String>) -> Self {
402 self.error = error.into_maybe_undefined();
403 self
404 }
405
406 #[must_use]
408 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
409 self.meta = meta.into_maybe_undefined();
410 self
411 }
412}
413
414#[cfg(feature = "unstable_session_compaction")]
422#[serde_as]
423#[skip_serializing_none]
424#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
426#[serde(rename_all = "camelCase")]
427#[non_exhaustive]
428pub struct CompactionSummaryChunk {
429 pub compaction_id: CompactionId,
431 pub content: ContentBlock,
433 #[serde_as(deserialize_as = "DefaultOnError")]
435 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
436 #[serde(default, rename = "_meta")]
437 pub meta: Option<Meta>,
438}
439
440#[cfg(feature = "unstable_session_compaction")]
441impl CompactionSummaryChunk {
442 #[must_use]
444 pub fn new(compaction_id: impl Into<CompactionId>, content: ContentBlock) -> Self {
445 Self {
446 compaction_id: compaction_id.into(),
447 content,
448 meta: None,
449 }
450 }
451
452 #[must_use]
454 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
455 self.meta = meta.into_option();
456 self
457 }
458}
459
460#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
466#[derive(Debug, Clone, Serialize, PartialEq)]
467#[cfg_attr(feature = "schemars", schemars(inline))]
468#[cfg_attr(feature = "schemars", schemars(transform = other_session_update_schema))]
469#[serde(rename_all = "camelCase")]
470#[non_exhaustive]
471pub struct OtherSessionUpdate {
472 #[serde(rename = "sessionUpdate")]
478 pub session_update: String,
479 #[serde(flatten)]
481 pub fields: BTreeMap<String, serde_json::Value>,
482}
483
484impl OtherSessionUpdate {
485 #[must_use]
487 pub fn new(
488 session_update: impl Into<String>,
489 mut fields: BTreeMap<String, serde_json::Value>,
490 ) -> Self {
491 fields.remove("sessionUpdate");
492 Self {
493 session_update: session_update.into(),
494 fields,
495 }
496 }
497}
498
499impl<'de> Deserialize<'de> for OtherSessionUpdate {
500 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
501 where
502 D: serde::Deserializer<'de>,
503 {
504 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
505 let session_update = fields
506 .remove("sessionUpdate")
507 .ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
508 let serde_json::Value::String(session_update) = session_update else {
509 return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
510 };
511
512 if is_known_session_update(&session_update) {
513 return Err(serde::de::Error::custom(format!(
514 "known session update `{session_update}` did not match its schema"
515 )));
516 }
517
518 Ok(Self {
519 session_update,
520 fields,
521 })
522 }
523}
524
525fn is_known_session_update(session_update: &str) -> bool {
526 #[cfg(feature = "unstable_session_notices")]
527 if session_update == "notice" {
528 return true;
529 }
530 #[cfg(feature = "unstable_session_compaction")]
531 if matches!(
532 session_update,
533 "compaction_update" | "compaction_summary_chunk"
534 ) {
535 return true;
536 }
537 #[cfg(feature = "unstable_plan_operations")]
538 if session_update == "plan_removed" {
539 return true;
540 }
541 matches!(
542 session_update,
543 "user_message_chunk"
544 | "user_message"
545 | "agent_message_chunk"
546 | "agent_message"
547 | "agent_thought_chunk"
548 | "agent_thought"
549 | "state_update"
550 | "tool_call_content_chunk"
551 | "tool_call_update"
552 | "terminal_update"
553 | "terminal_output_chunk"
554 | "plan_update"
555 | "available_commands_update"
556 | "config_option_update"
557 | "session_info_update"
558 | "usage_update"
559 )
560}
561
562#[cfg(feature = "schemars")]
563fn other_session_update_schema(schema: &mut Schema) {
564 super::schema_util::reject_known_string_discriminators(
565 schema,
566 "sessionUpdate",
567 &[
568 "user_message_chunk",
569 "user_message",
570 "agent_message_chunk",
571 "agent_message",
572 "agent_thought_chunk",
573 "agent_thought",
574 "state_update",
575 "tool_call_content_chunk",
576 "tool_call_update",
577 "terminal_update",
578 "terminal_output_chunk",
579 "plan_update",
580 "available_commands_update",
581 "config_option_update",
582 "session_info_update",
583 #[cfg(feature = "unstable_plan_operations")]
584 "plan_removed",
585 "usage_update",
586 #[cfg(feature = "unstable_session_notices")]
587 "notice",
588 #[cfg(feature = "unstable_session_compaction")]
589 "compaction_update",
590 #[cfg(feature = "unstable_session_compaction")]
591 "compaction_summary_chunk",
592 ],
593 );
594}
595
596#[serde_as]
598#[skip_serializing_none]
599#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
601#[serde(rename_all = "camelCase")]
602#[non_exhaustive]
603pub struct ConfigOptionUpdate {
604 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
606 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
607 pub config_options: Vec<SessionConfigOption>,
608 #[serde_as(deserialize_as = "DefaultOnError")]
614 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
615 #[serde(default)]
616 #[serde(rename = "_meta")]
617 pub meta: Option<Meta>,
618}
619
620impl ConfigOptionUpdate {
621 #[must_use]
623 pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
624 Self {
625 config_options,
626 meta: None,
627 }
628 }
629
630 #[must_use]
636 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
637 self.meta = meta.into_option();
638 self
639 }
640}
641
642#[serde_as]
650#[skip_serializing_none]
651#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
652#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
653#[serde(rename_all = "camelCase")]
654#[non_exhaustive]
655pub struct SessionInfoUpdate {
656 #[serde_as(deserialize_as = "DefaultOnError")]
658 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
659 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
660 pub title: MaybeUndefined<String>,
661 #[serde_as(deserialize_as = "DefaultOnError")]
663 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
664 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
665 pub updated_at: MaybeUndefined<String>,
666 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
672 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
673 #[serde(
674 rename = "_meta",
675 default,
676 skip_serializing_if = "MaybeUndefined::is_undefined"
677 )]
678 pub meta: MaybeUndefined<Meta>,
679}
680
681impl SessionInfoUpdate {
682 #[must_use]
684 pub fn new() -> Self {
685 Self::default()
686 }
687
688 #[must_use]
690 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
691 self.title = title.into_maybe_undefined();
692 self
693 }
694
695 #[must_use]
697 pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
698 self.updated_at = updated_at.into_maybe_undefined();
699 self
700 }
701
702 #[must_use]
708 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
709 self.meta = meta.into_maybe_undefined();
710 self
711 }
712}
713
714#[serde_as]
716#[skip_serializing_none]
717#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
718#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
719#[serde(rename_all = "camelCase")]
720#[non_exhaustive]
721pub struct UsageUpdate {
722 pub used: u64,
724 pub size: u64,
726 #[serde_as(deserialize_as = "DefaultOnError")]
728 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
729 #[serde(default)]
730 pub cost: Option<Cost>,
731 #[serde_as(deserialize_as = "DefaultOnError")]
737 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
738 #[serde(default)]
739 #[serde(rename = "_meta")]
740 pub meta: Option<Meta>,
741}
742
743impl UsageUpdate {
744 #[must_use]
746 pub fn new(used: u64, size: u64) -> Self {
747 Self {
748 used,
749 size,
750 cost: None,
751 meta: None,
752 }
753 }
754
755 #[must_use]
757 pub fn cost(mut self, cost: impl IntoOption<Cost>) -> Self {
758 self.cost = cost.into_option();
759 self
760 }
761
762 #[must_use]
768 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
769 self.meta = meta.into_option();
770 self
771 }
772}
773
774#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
779#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
780#[serde(tag = "state", rename_all = "snake_case")]
781#[non_exhaustive]
782pub enum StateUpdate {
783 Running(RunningStateUpdate),
785 Idle(IdleStateUpdate),
787 RequiresAction(RequiresActionStateUpdate),
789 #[serde(untagged)]
795 Other(OtherStateUpdate),
796}
797
798#[serde_as]
800#[skip_serializing_none]
801#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
802#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
803#[serde(rename_all = "camelCase")]
804#[non_exhaustive]
805pub struct RunningStateUpdate {
806 #[serde_as(deserialize_as = "DefaultOnError")]
812 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
813 #[serde(default)]
814 #[serde(rename = "_meta")]
815 pub meta: Option<Meta>,
816}
817
818impl RunningStateUpdate {
819 #[must_use]
821 pub fn new() -> Self {
822 Self::default()
823 }
824
825 #[must_use]
831 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
832 self.meta = meta.into_option();
833 self
834 }
835}
836
837#[serde_as]
839#[skip_serializing_none]
840#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
841#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
842#[serde(rename_all = "camelCase")]
843#[non_exhaustive]
844pub struct IdleStateUpdate {
845 #[serde_as(deserialize_as = "DefaultOnError")]
850 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
851 #[serde(default)]
852 pub stop_reason: Option<StopReason>,
853 #[cfg(feature = "unstable_end_turn_token_usage")]
862 #[serde_as(deserialize_as = "DefaultOnError")]
863 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
864 #[serde(default)]
865 pub usage: Option<Usage>,
866 #[serde_as(deserialize_as = "DefaultOnError")]
872 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
873 #[serde(default)]
874 #[serde(rename = "_meta")]
875 pub meta: Option<Meta>,
876}
877
878impl IdleStateUpdate {
879 #[must_use]
881 pub fn new() -> Self {
882 Self::default()
883 }
884
885 #[must_use]
887 pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
888 self.stop_reason = stop_reason.into_option();
889 self
890 }
891
892 #[cfg(feature = "unstable_end_turn_token_usage")]
898 #[must_use]
899 pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
900 self.usage = usage.into_option();
901 self
902 }
903
904 #[must_use]
910 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
911 self.meta = meta.into_option();
912 self
913 }
914}
915
916#[serde_as]
918#[skip_serializing_none]
919#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
920#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
921#[serde(rename_all = "camelCase")]
922#[non_exhaustive]
923pub struct RequiresActionStateUpdate {
924 #[serde_as(deserialize_as = "DefaultOnError")]
930 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
931 #[serde(default)]
932 #[serde(rename = "_meta")]
933 pub meta: Option<Meta>,
934}
935
936impl RequiresActionStateUpdate {
937 #[must_use]
939 pub fn new() -> Self {
940 Self::default()
941 }
942
943 #[must_use]
949 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
950 self.meta = meta.into_option();
951 self
952 }
953}
954
955#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
960#[derive(Debug, Clone, Serialize, PartialEq)]
961#[cfg_attr(feature = "schemars", schemars(inline))]
962#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
963#[serde(rename_all = "camelCase")]
964#[non_exhaustive]
965pub struct OtherStateUpdate {
966 #[serde(rename = "state")]
972 pub state: String,
973 #[serde(flatten)]
975 pub fields: BTreeMap<String, serde_json::Value>,
976}
977
978impl OtherStateUpdate {
979 #[must_use]
981 pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
982 fields.remove("state");
983 Self {
984 state: state.into(),
985 fields,
986 }
987 }
988}
989
990impl<'de> Deserialize<'de> for OtherStateUpdate {
991 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
992 where
993 D: serde::Deserializer<'de>,
994 {
995 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
996 let state = fields
997 .remove("state")
998 .ok_or_else(|| serde::de::Error::missing_field("state"))?;
999 let serde_json::Value::String(state) = state else {
1000 return Err(serde::de::Error::custom("`state` must be a string"));
1001 };
1002
1003 if is_known_state_update(&state) {
1004 return Err(serde::de::Error::custom(format!(
1005 "known state update `{state}` did not match its schema"
1006 )));
1007 }
1008
1009 Ok(Self { state, fields })
1010 }
1011}
1012
1013fn is_known_state_update(state: &str) -> bool {
1014 matches!(state, "running" | "idle" | "requires_action")
1015}
1016
1017#[cfg(feature = "schemars")]
1018fn other_state_update_schema(schema: &mut Schema) {
1019 super::schema_util::reject_known_string_discriminators(
1020 schema,
1021 "state",
1022 &["running", "idle", "requires_action"],
1023 );
1024}
1025
1026#[serde_as]
1028#[skip_serializing_none]
1029#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1030#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1031#[serde(rename_all = "camelCase")]
1032#[non_exhaustive]
1033pub struct Cost {
1034 pub amount: f64,
1036 #[cfg_attr(feature = "schemars", schemars(pattern(r"^[A-Z]{3}$")))]
1038 pub currency: String,
1039 #[serde_as(deserialize_as = "DefaultOnError")]
1045 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1046 #[serde(default)]
1047 #[serde(rename = "_meta")]
1048 pub meta: Option<Meta>,
1049}
1050
1051impl Cost {
1052 #[must_use]
1054 pub fn new(amount: f64, currency: impl Into<String>) -> Self {
1055 Self {
1056 amount,
1057 currency: currency.into(),
1058 meta: None,
1059 }
1060 }
1061
1062 #[must_use]
1068 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1069 self.meta = meta.into_option();
1070 self
1071 }
1072}
1073
1074#[serde_as]
1076#[skip_serializing_none]
1077#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1078#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1079#[serde(rename_all = "camelCase")]
1080#[non_exhaustive]
1081pub struct ContentChunk {
1082 pub message_id: MessageId,
1087 pub content: ContentBlock,
1089 #[serde_as(deserialize_as = "DefaultOnError")]
1095 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1096 #[serde(default)]
1097 #[serde(rename = "_meta")]
1098 pub meta: Option<Meta>,
1099}
1100
1101impl ContentChunk {
1102 #[must_use]
1104 pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
1105 Self {
1106 content,
1107 message_id: message_id.into(),
1108 meta: None,
1109 }
1110 }
1111
1112 #[must_use]
1118 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1119 self.meta = meta.into_option();
1120 self
1121 }
1122}
1123
1124#[serde_as]
1138#[skip_serializing_none]
1139#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1141#[serde(rename_all = "camelCase")]
1142#[non_exhaustive]
1143pub struct UserMessage {
1144 pub message_id: MessageId,
1146 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
1148 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1149 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1150 pub content: MaybeUndefined<Vec<ContentBlock>>,
1151 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1157 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1158 #[serde(
1159 rename = "_meta",
1160 default,
1161 skip_serializing_if = "MaybeUndefined::is_undefined"
1162 )]
1163 pub meta: MaybeUndefined<Meta>,
1164}
1165
1166impl UserMessage {
1167 #[must_use]
1169 pub fn new(message_id: impl Into<MessageId>) -> Self {
1170 Self {
1171 message_id: message_id.into(),
1172 content: MaybeUndefined::Undefined,
1173 meta: MaybeUndefined::Undefined,
1174 }
1175 }
1176
1177 #[must_use]
1179 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
1180 self.content = content.into_maybe_undefined();
1181 self
1182 }
1183
1184 #[must_use]
1190 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1191 self.meta = meta.into_maybe_undefined();
1192 self
1193 }
1194}
1195
1196#[serde_as]
1210#[skip_serializing_none]
1211#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1213#[serde(rename_all = "camelCase")]
1214#[non_exhaustive]
1215pub struct AgentMessage {
1216 pub message_id: MessageId,
1218 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
1220 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1221 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1222 pub content: MaybeUndefined<Vec<ContentBlock>>,
1223 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1229 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1230 #[serde(
1231 rename = "_meta",
1232 default,
1233 skip_serializing_if = "MaybeUndefined::is_undefined"
1234 )]
1235 pub meta: MaybeUndefined<Meta>,
1236}
1237
1238impl AgentMessage {
1239 #[must_use]
1241 pub fn new(message_id: impl Into<MessageId>) -> Self {
1242 Self {
1243 message_id: message_id.into(),
1244 content: MaybeUndefined::Undefined,
1245 meta: MaybeUndefined::Undefined,
1246 }
1247 }
1248
1249 #[must_use]
1251 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
1252 self.content = content.into_maybe_undefined();
1253 self
1254 }
1255
1256 #[must_use]
1262 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1263 self.meta = meta.into_maybe_undefined();
1264 self
1265 }
1266}
1267
1268#[serde_as]
1282#[skip_serializing_none]
1283#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1285#[serde(rename_all = "camelCase")]
1286#[non_exhaustive]
1287pub struct AgentThought {
1288 pub message_id: MessageId,
1290 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
1292 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1293 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1294 pub content: MaybeUndefined<Vec<ContentBlock>>,
1295 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1301 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1302 #[serde(
1303 rename = "_meta",
1304 default,
1305 skip_serializing_if = "MaybeUndefined::is_undefined"
1306 )]
1307 pub meta: MaybeUndefined<Meta>,
1308}
1309
1310impl AgentThought {
1311 #[must_use]
1313 pub fn new(message_id: impl Into<MessageId>) -> Self {
1314 Self {
1315 message_id: message_id.into(),
1316 content: MaybeUndefined::Undefined,
1317 meta: MaybeUndefined::Undefined,
1318 }
1319 }
1320
1321 #[must_use]
1323 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
1324 self.content = content.into_maybe_undefined();
1325 self
1326 }
1327
1328 #[must_use]
1334 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1335 self.meta = meta.into_maybe_undefined();
1336 self
1337 }
1338}
1339
1340#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
1343#[serde(transparent)]
1344#[from(forward)]
1345#[non_exhaustive]
1346pub struct MessageId(pub Arc<str>);
1347
1348impl MessageId {
1349 #[must_use]
1351 pub fn new(id: impl Into<Self>) -> Self {
1352 id.into()
1353 }
1354}
1355
1356#[serde_as]
1358#[skip_serializing_none]
1359#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1361#[serde(rename_all = "camelCase")]
1362#[non_exhaustive]
1363pub struct AvailableCommandsUpdate {
1364 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
1366 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1367 pub available_commands: Vec<AvailableCommand>,
1368 #[serde_as(deserialize_as = "DefaultOnError")]
1374 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1375 #[serde(default)]
1376 #[serde(rename = "_meta")]
1377 pub meta: Option<Meta>,
1378}
1379
1380impl AvailableCommandsUpdate {
1381 #[must_use]
1383 pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
1384 Self {
1385 available_commands,
1386 meta: None,
1387 }
1388 }
1389
1390 #[must_use]
1396 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1397 self.meta = meta.into_option();
1398 self
1399 }
1400}
1401
1402#[serde_as]
1404#[skip_serializing_none]
1405#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1407#[serde(rename_all = "camelCase")]
1408#[non_exhaustive]
1409pub struct AvailableCommand {
1410 pub name: String,
1412 pub description: String,
1414 #[serde_as(deserialize_as = "DefaultOnError")]
1416 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1417 #[serde(default)]
1418 pub input: Option<AvailableCommandInput>,
1419 #[serde_as(deserialize_as = "DefaultOnError")]
1425 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1426 #[serde(default)]
1427 #[serde(rename = "_meta")]
1428 pub meta: Option<Meta>,
1429}
1430
1431impl AvailableCommand {
1432 #[must_use]
1434 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
1435 Self {
1436 name: name.into(),
1437 description: description.into(),
1438 input: None,
1439 meta: None,
1440 }
1441 }
1442
1443 #[must_use]
1445 pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
1446 self.input = input.into_option();
1447 self
1448 }
1449
1450 #[must_use]
1456 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1457 self.meta = meta.into_option();
1458 self
1459 }
1460}
1461
1462#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1465#[serde(tag = "type", rename_all = "snake_case")]
1466#[non_exhaustive]
1467pub enum AvailableCommandInput {
1468 #[serde(rename = "text")]
1470 Text(TextCommandInput),
1471 #[serde(untagged)]
1482 Other(OtherAvailableCommandInput),
1483}
1484
1485#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1487#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1488#[cfg_attr(feature = "schemars", schemars(inline))]
1489#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
1490#[serde(rename_all = "camelCase")]
1491#[non_exhaustive]
1492pub struct OtherAvailableCommandInput {
1493 #[serde(rename = "type")]
1499 pub type_: String,
1500 #[serde(flatten)]
1502 pub fields: BTreeMap<String, serde_json::Value>,
1503}
1504
1505impl OtherAvailableCommandInput {
1506 #[must_use]
1508 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1509 fields.remove("type");
1510 Self {
1511 type_: type_.into(),
1512 fields,
1513 }
1514 }
1515}
1516
1517impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
1518 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1519 where
1520 D: serde::Deserializer<'de>,
1521 {
1522 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1523 let type_ = fields
1524 .remove("type")
1525 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
1526 let serde_json::Value::String(type_) = type_ else {
1527 return Err(serde::de::Error::custom("`type` must be a string"));
1528 };
1529
1530 if is_known_available_command_input_type(&type_) {
1531 return Err(serde::de::Error::custom(format!(
1532 "known available command input type `{type_}` did not match its schema"
1533 )));
1534 }
1535
1536 Ok(Self { type_, fields })
1537 }
1538}
1539
1540const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
1541
1542fn is_known_available_command_input_type(type_: &str) -> bool {
1543 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
1544}
1545
1546#[cfg(feature = "schemars")]
1547fn other_available_command_input_schema(schema: &mut Schema) {
1548 super::schema_util::reject_known_string_discriminators(
1549 schema,
1550 "type",
1551 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
1552 );
1553}
1554
1555#[serde_as]
1557#[skip_serializing_none]
1558#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1559#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1560#[serde(rename_all = "camelCase")]
1561#[non_exhaustive]
1562pub struct TextCommandInput {
1563 pub hint: String,
1565 #[serde_as(deserialize_as = "DefaultOnError")]
1571 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1572 #[serde(default)]
1573 #[serde(rename = "_meta")]
1574 pub meta: Option<Meta>,
1575}
1576
1577impl TextCommandInput {
1578 #[must_use]
1580 pub fn new(hint: impl Into<String>) -> Self {
1581 Self {
1582 hint: hint.into(),
1583 meta: None,
1584 }
1585 }
1586
1587 #[must_use]
1593 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1594 self.meta = meta.into_option();
1595 self
1596 }
1597}
1598
1599#[serde_as]
1607#[skip_serializing_none]
1608#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1609#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1610#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
1611#[serde(rename_all = "camelCase")]
1612#[non_exhaustive]
1613pub struct RequestPermissionRequest {
1614 pub session_id: SessionId,
1616 pub title: String,
1621 #[serde_as(deserialize_as = "DefaultOnError")]
1627 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1628 #[serde(default)]
1629 pub description: Option<String>,
1630 #[serde(default)]
1634 pub subject: Option<RequestPermissionSubject>,
1635 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
1638 pub options: Vec<PermissionOption>,
1639 #[serde_as(deserialize_as = "DefaultOnError")]
1645 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1646 #[serde(default)]
1647 #[serde(rename = "_meta")]
1648 pub meta: Option<Meta>,
1649}
1650
1651impl RequestPermissionRequest {
1652 #[must_use]
1654 pub fn new(
1655 session_id: impl Into<SessionId>,
1656 title: impl Into<String>,
1657 options: Vec<PermissionOption>,
1658 ) -> Self {
1659 Self {
1660 session_id: session_id.into(),
1661 title: title.into(),
1662 description: None,
1663 subject: None,
1664 options,
1665 meta: None,
1666 }
1667 }
1668
1669 #[must_use]
1671 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
1672 self.description = description.into_option();
1673 self
1674 }
1675
1676 #[must_use]
1678 pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
1679 self.subject = subject.into_option();
1680 self
1681 }
1682
1683 #[must_use]
1689 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1690 self.meta = meta.into_option();
1691 self
1692 }
1693}
1694
1695#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1697#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1698#[serde(tag = "type", rename_all = "snake_case")]
1699#[non_exhaustive]
1700pub enum RequestPermissionSubject {
1701 ToolCall(Box<ToolCallPermissionSubject>),
1703 Command(CommandPermissionSubject),
1705 #[serde(untagged)]
1716 Other(OtherRequestPermissionSubject),
1717}
1718
1719impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
1720 fn from(subject: ToolCallPermissionSubject) -> Self {
1721 Self::ToolCall(Box::new(subject))
1722 }
1723}
1724
1725impl From<ToolCallUpdate> for RequestPermissionSubject {
1726 fn from(tool_call: ToolCallUpdate) -> Self {
1727 ToolCallPermissionSubject::new(tool_call).into()
1728 }
1729}
1730
1731impl From<CommandPermissionSubject> for RequestPermissionSubject {
1732 fn from(subject: CommandPermissionSubject) -> Self {
1733 Self::Command(subject)
1734 }
1735}
1736
1737#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1739#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1740#[serde(rename_all = "camelCase")]
1741#[non_exhaustive]
1742pub struct ToolCallPermissionSubject {
1743 pub tool_call: ToolCallUpdate,
1745}
1746
1747impl ToolCallPermissionSubject {
1748 #[must_use]
1750 pub fn new(tool_call: ToolCallUpdate) -> Self {
1751 Self { tool_call }
1752 }
1753}
1754
1755#[serde_as]
1757#[skip_serializing_none]
1758#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1759#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1760#[serde(rename_all = "camelCase")]
1761#[non_exhaustive]
1762pub struct CommandPermissionSubject {
1763 pub command: String,
1765 pub cwd: AbsolutePath,
1767 #[serde_as(deserialize_as = "DefaultOnError")]
1769 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1770 #[serde(default)]
1771 pub tool_call_id: Option<ToolCallId>,
1772 #[serde_as(deserialize_as = "DefaultOnError")]
1774 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1775 #[serde(default)]
1776 pub terminal_id: Option<TerminalId>,
1777 #[serde_as(deserialize_as = "DefaultOnError")]
1783 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1784 #[serde(default)]
1785 #[serde(rename = "_meta")]
1786 pub meta: Option<Meta>,
1787}
1788
1789impl CommandPermissionSubject {
1790 #[must_use]
1792 pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
1793 Self {
1794 command: command.into(),
1795 cwd: cwd.into(),
1796 tool_call_id: None,
1797 terminal_id: None,
1798 meta: None,
1799 }
1800 }
1801
1802 #[must_use]
1804 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
1805 self.tool_call_id = tool_call_id.into_option();
1806 self
1807 }
1808
1809 #[must_use]
1811 pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
1812 self.terminal_id = terminal_id.into_option();
1813 self
1814 }
1815
1816 #[must_use]
1818 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1819 self.meta = meta.into_option();
1820 self
1821 }
1822}
1823
1824#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1826#[derive(Debug, Clone, Serialize, PartialEq)]
1827#[cfg_attr(feature = "schemars", schemars(inline))]
1828#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
1829#[serde(rename_all = "camelCase")]
1830#[non_exhaustive]
1831pub struct OtherRequestPermissionSubject {
1832 #[serde(rename = "type")]
1838 pub type_: String,
1839 #[serde(flatten)]
1841 pub fields: BTreeMap<String, serde_json::Value>,
1842}
1843
1844impl OtherRequestPermissionSubject {
1845 #[must_use]
1847 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
1848 fields.remove("type");
1849 Self {
1850 type_: type_.into(),
1851 fields,
1852 }
1853 }
1854}
1855
1856impl<'de> Deserialize<'de> for OtherRequestPermissionSubject {
1857 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1858 where
1859 D: serde::Deserializer<'de>,
1860 {
1861 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1862 let type_ = fields
1863 .remove("type")
1864 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
1865 let serde_json::Value::String(type_) = type_ else {
1866 return Err(serde::de::Error::custom("`type` must be a string"));
1867 };
1868
1869 if is_known_request_permission_subject_type(&type_) {
1870 return Err(serde::de::Error::custom(format!(
1871 "known request permission subject `{type_}` did not match its schema"
1872 )));
1873 }
1874
1875 Ok(Self { type_, fields })
1876 }
1877}
1878
1879fn is_known_request_permission_subject_type(type_: &str) -> bool {
1880 matches!(type_, "tool_call" | "command")
1881}
1882
1883#[cfg(feature = "schemars")]
1884fn other_request_permission_subject_schema(schema: &mut Schema) {
1885 super::schema_util::reject_known_string_discriminators(
1886 schema,
1887 "type",
1888 &["tool_call", "command"],
1889 );
1890}
1891
1892#[serde_as]
1894#[skip_serializing_none]
1895#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1896#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1897#[serde(rename_all = "camelCase")]
1898#[non_exhaustive]
1899pub struct PermissionOption {
1900 pub option_id: PermissionOptionId,
1902 pub name: String,
1904 pub kind: PermissionOptionKind,
1906 #[serde_as(deserialize_as = "DefaultOnError")]
1912 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1913 #[serde(default)]
1914 #[serde(rename = "_meta")]
1915 pub meta: Option<Meta>,
1916}
1917
1918impl PermissionOption {
1919 #[must_use]
1921 pub fn new(
1922 option_id: impl Into<PermissionOptionId>,
1923 name: impl Into<String>,
1924 kind: PermissionOptionKind,
1925 ) -> Self {
1926 Self {
1927 option_id: option_id.into(),
1928 name: name.into(),
1929 kind,
1930 meta: None,
1931 }
1932 }
1933
1934 #[must_use]
1940 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1941 self.meta = meta.into_option();
1942 self
1943 }
1944}
1945
1946#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1948#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
1949#[serde(transparent)]
1950#[from(forward)]
1951#[non_exhaustive]
1952pub struct PermissionOptionId(pub Arc<str>);
1953
1954impl PermissionOptionId {
1955 #[must_use]
1957 pub fn new(id: impl Into<Self>) -> Self {
1958 id.into()
1959 }
1960}
1961
1962#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1966#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1967#[serde(rename_all = "snake_case")]
1968#[non_exhaustive]
1969pub enum PermissionOptionKind {
1970 AllowOnce,
1972 AllowAlways,
1974 RejectOnce,
1976 RejectAlways,
1978 #[serde(untagged)]
1984 Other(String),
1985}
1986
1987#[serde_as]
1989#[skip_serializing_none]
1990#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1991#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1992#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
1993#[serde(rename_all = "camelCase")]
1994#[non_exhaustive]
1995pub struct RequestPermissionResponse {
1996 pub outcome: RequestPermissionOutcome,
1998 #[serde_as(deserialize_as = "DefaultOnError")]
2004 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2005 #[serde(default)]
2006 #[serde(rename = "_meta")]
2007 pub meta: Option<Meta>,
2008}
2009
2010impl RequestPermissionResponse {
2011 #[must_use]
2013 pub fn new(outcome: RequestPermissionOutcome) -> Self {
2014 Self {
2015 outcome,
2016 meta: None,
2017 }
2018 }
2019
2020 #[must_use]
2026 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2027 self.meta = meta.into_option();
2028 self
2029 }
2030}
2031
2032#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2034#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2035#[serde(tag = "outcome", rename_all = "snake_case")]
2036#[non_exhaustive]
2037pub enum RequestPermissionOutcome {
2038 Cancelled,
2046 #[serde(rename_all = "camelCase")]
2048 Selected(SelectedPermissionOutcome),
2049 #[serde(untagged)]
2060 Other(OtherRequestPermissionOutcome),
2061}
2062
2063#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2069#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2070#[cfg_attr(feature = "schemars", schemars(inline))]
2071#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
2072#[serde(rename_all = "camelCase")]
2073#[non_exhaustive]
2074pub struct OtherRequestPermissionOutcome {
2075 pub outcome: String,
2081 #[serde(flatten)]
2083 pub fields: BTreeMap<String, serde_json::Value>,
2084}
2085
2086impl OtherRequestPermissionOutcome {
2087 #[must_use]
2089 pub fn new(
2090 outcome: impl Into<String>,
2091 mut fields: BTreeMap<String, serde_json::Value>,
2092 ) -> Self {
2093 fields.remove("outcome");
2094 Self {
2095 outcome: outcome.into(),
2096 fields,
2097 }
2098 }
2099}
2100
2101impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
2102 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2103 where
2104 D: serde::Deserializer<'de>,
2105 {
2106 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2107 let outcome = fields
2108 .remove("outcome")
2109 .ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
2110 let serde_json::Value::String(outcome) = outcome else {
2111 return Err(serde::de::Error::custom("`outcome` must be a string"));
2112 };
2113
2114 if is_known_request_permission_outcome(&outcome) {
2115 return Err(serde::de::Error::custom(format!(
2116 "known request permission outcome `{outcome}` did not match its schema"
2117 )));
2118 }
2119
2120 Ok(Self { outcome, fields })
2121 }
2122}
2123
2124fn is_known_request_permission_outcome(outcome: &str) -> bool {
2125 matches!(outcome, "cancelled" | "selected")
2126}
2127
2128#[cfg(feature = "schemars")]
2129fn other_request_permission_outcome_schema(schema: &mut Schema) {
2130 super::schema_util::reject_known_string_discriminators(
2131 schema,
2132 "outcome",
2133 &["cancelled", "selected"],
2134 );
2135}
2136
2137#[serde_as]
2139#[skip_serializing_none]
2140#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2142#[serde(rename_all = "camelCase")]
2143#[non_exhaustive]
2144pub struct SelectedPermissionOutcome {
2145 pub option_id: PermissionOptionId,
2147 #[serde_as(deserialize_as = "DefaultOnError")]
2153 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2154 #[serde(default)]
2155 #[serde(rename = "_meta")]
2156 pub meta: Option<Meta>,
2157}
2158
2159impl SelectedPermissionOutcome {
2160 #[must_use]
2162 pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
2163 Self {
2164 option_id: option_id.into(),
2165 meta: None,
2166 }
2167 }
2168
2169 #[must_use]
2175 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2176 self.meta = meta.into_option();
2177 self
2178 }
2179}
2180
2181#[serde_as]
2190#[skip_serializing_none]
2191#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2192#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2193#[serde(rename_all = "camelCase")]
2194#[non_exhaustive]
2195pub struct ClientCapabilities {
2196 #[serde_as(deserialize_as = "DefaultOnError")]
2203 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2204 #[serde(default)]
2205 pub auth: Option<AuthCapabilities>,
2206 #[serde_as(deserialize_as = "DefaultOnError")]
2212 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2213 #[serde(default)]
2214 pub elicitation: Option<ElicitationCapabilities>,
2215 #[cfg(feature = "unstable_nes")]
2224 #[serde_as(deserialize_as = "DefaultOnError")]
2225 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2226 #[serde(default)]
2227 pub nes: Option<ClientNesCapabilities>,
2228 #[cfg(feature = "unstable_nes")]
2234 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
2235 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2237 pub position_encodings: Vec<PositionEncodingKind>,
2238
2239 #[serde_as(deserialize_as = "DefaultOnError")]
2245 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2246 #[serde(default)]
2247 #[serde(rename = "_meta")]
2248 pub meta: Option<Meta>,
2249}
2250
2251impl ClientCapabilities {
2252 #[must_use]
2254 pub fn new() -> Self {
2255 Self::default()
2256 }
2257
2258 #[must_use]
2262 pub fn auth(mut self, auth: impl IntoOption<AuthCapabilities>) -> Self {
2263 self.auth = auth.into_option();
2264 self
2265 }
2266
2267 #[must_use]
2270 pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
2271 self.elicitation = elicitation.into_option();
2272 self
2273 }
2274
2275 #[cfg(feature = "unstable_nes")]
2279 #[must_use]
2280 pub fn nes(mut self, nes: impl IntoOption<ClientNesCapabilities>) -> Self {
2281 self.nes = nes.into_option();
2282 self
2283 }
2284
2285 #[cfg(feature = "unstable_nes")]
2289 #[must_use]
2290 pub fn position_encodings(mut self, position_encodings: Vec<PositionEncodingKind>) -> Self {
2291 self.position_encodings = position_encodings;
2292 self
2293 }
2294
2295 #[must_use]
2301 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2302 self.meta = meta.into_option();
2303 self
2304 }
2305}
2306
2307#[serde_as]
2313#[skip_serializing_none]
2314#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2315#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2316#[serde(rename_all = "camelCase")]
2317#[non_exhaustive]
2318pub struct AuthCapabilities {
2319 #[serde_as(deserialize_as = "DefaultOnError")]
2326 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2327 #[serde(default)]
2328 pub terminal: Option<TerminalAuthCapabilities>,
2329 #[serde_as(deserialize_as = "DefaultOnError")]
2335 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2336 #[serde(default)]
2337 #[serde(rename = "_meta")]
2338 pub meta: Option<Meta>,
2339}
2340
2341impl AuthCapabilities {
2342 #[must_use]
2344 pub fn new() -> Self {
2345 Self::default()
2346 }
2347
2348 #[must_use]
2356 pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
2357 self.terminal = terminal.into_option();
2358 self
2359 }
2360
2361 #[must_use]
2367 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2368 self.meta = meta.into_option();
2369 self
2370 }
2371}
2372
2373#[serde_as]
2379#[skip_serializing_none]
2380#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2381#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2382#[non_exhaustive]
2383pub struct TerminalAuthCapabilities {
2384 #[serde_as(deserialize_as = "DefaultOnError")]
2390 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2391 #[serde(default)]
2392 #[serde(rename = "_meta")]
2393 pub meta: Option<Meta>,
2394}
2395
2396impl TerminalAuthCapabilities {
2397 #[must_use]
2399 pub fn new() -> Self {
2400 Self::default()
2401 }
2402
2403 #[must_use]
2409 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2410 self.meta = meta.into_option();
2411 self
2412 }
2413}
2414
2415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2421#[non_exhaustive]
2422pub struct ClientMethodNames {
2423 pub session_request_permission: &'static str,
2425 pub session_update: &'static str,
2427 #[cfg(feature = "unstable_mcp_over_acp")]
2429 pub mcp_connect: &'static str,
2430 #[cfg(feature = "unstable_mcp_over_acp")]
2432 pub mcp_message: &'static str,
2433 #[cfg(feature = "unstable_mcp_over_acp")]
2435 pub mcp_disconnect: &'static str,
2436 pub elicitation_create: &'static str,
2438 pub elicitation_complete: &'static str,
2440}
2441
2442pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
2444 session_update: SESSION_UPDATE_NOTIFICATION,
2445 session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
2446 #[cfg(feature = "unstable_mcp_over_acp")]
2447 mcp_connect: MCP_CONNECT_METHOD_NAME,
2448 #[cfg(feature = "unstable_mcp_over_acp")]
2449 mcp_message: MCP_MESSAGE_METHOD_NAME,
2450 #[cfg(feature = "unstable_mcp_over_acp")]
2451 mcp_disconnect: MCP_DISCONNECT_METHOD_NAME,
2452 elicitation_create: ELICITATION_CREATE_METHOD_NAME,
2453 elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
2454};
2455
2456pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
2458pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
2460pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
2462pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
2464
2465#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2472#[derive(Clone, Debug, Serialize, Deserialize)]
2473#[serde(untagged)]
2474#[cfg_attr(feature = "schemars", schemars(inline))]
2475#[non_exhaustive]
2476pub enum AgentRequest {
2477 RequestPermissionRequest(Box<RequestPermissionRequest>),
2488 CreateElicitationRequest(Box<CreateElicitationRequest>),
2492 #[cfg(feature = "unstable_mcp_over_acp")]
2498 ConnectMcpRequest(Box<ConnectMcpRequest>),
2499 #[cfg(feature = "unstable_mcp_over_acp")]
2505 MessageMcpRequest(Box<MessageMcpRequest>),
2506 #[cfg(feature = "unstable_mcp_over_acp")]
2512 DisconnectMcpRequest(Box<DisconnectMcpRequest>),
2513 ExtMethodRequest(Box<ExtRequest>),
2521}
2522
2523impl AgentRequest {
2524 #[must_use]
2526 pub fn method(&self) -> &str {
2527 match self {
2528 Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
2529 Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
2530 #[cfg(feature = "unstable_mcp_over_acp")]
2531 Self::ConnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_connect,
2532 #[cfg(feature = "unstable_mcp_over_acp")]
2533 Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
2534 #[cfg(feature = "unstable_mcp_over_acp")]
2535 Self::DisconnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_disconnect,
2536 Self::ExtMethodRequest(ext_request) => &ext_request.method,
2537 }
2538 }
2539}
2540
2541#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2548#[derive(Clone, Debug, Serialize, Deserialize)]
2549#[serde(untagged)]
2550#[cfg_attr(feature = "schemars", schemars(inline))]
2551#[non_exhaustive]
2552pub enum ClientResponse {
2553 RequestPermissionResponse(Box<RequestPermissionResponse>),
2555 CreateElicitationResponse(Box<CreateElicitationResponse>),
2557 #[cfg(feature = "unstable_mcp_over_acp")]
2559 ConnectMcpResponse(Box<ConnectMcpResponse>),
2560 #[cfg(feature = "unstable_mcp_over_acp")]
2562 DisconnectMcpResponse(#[serde(default)] Box<DisconnectMcpResponse>),
2563 #[cfg(feature = "unstable_mcp_over_acp")]
2565 MessageMcpResponse(Box<MessageMcpResponse>),
2566 ExtMethodResponse(Box<ExtResponse>),
2568}
2569
2570#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2577#[derive(Clone, Debug, Serialize, Deserialize)]
2578#[serde(untagged)]
2579#[cfg_attr(feature = "schemars", schemars(inline))]
2580#[non_exhaustive]
2581pub enum AgentNotification {
2582 UpdateSessionNotification(Box<UpdateSessionNotification>),
2595 CompleteElicitationNotification(Box<CompleteElicitationNotification>),
2599 #[cfg(feature = "unstable_mcp_over_acp")]
2605 MessageMcpNotification(Box<MessageMcpNotification>),
2606 ExtNotification(Box<ExtNotification>),
2614}
2615
2616impl AgentNotification {
2617 #[must_use]
2619 pub fn method(&self) -> &str {
2620 match self {
2621 Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
2622 Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
2623 #[cfg(feature = "unstable_mcp_over_acp")]
2624 Self::MessageMcpNotification(_) => CLIENT_METHOD_NAMES.mcp_message,
2625 Self::ExtNotification(ext_notification) => &ext_notification.method,
2626 }
2627 }
2628}
2629
2630#[cfg(test)]
2631mod tests {
2632 use super::*;
2633
2634 #[cfg(feature = "unstable_session_notices")]
2635 #[test]
2636 fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
2637 use serde_json::json;
2638
2639 let mut meta = Meta::new();
2640 meta.insert("source".into(), json!("fallback"));
2641 let v2_notice = SessionUpdate::Notice(
2642 Notice::new(NoticeSeverity::Error, "Provider degraded")
2643 .description("Requests may take longer than usual.")
2644 .meta(meta.clone()),
2645 );
2646 let expected = json!({
2647 "sessionUpdate": "notice",
2648 "severity": "error",
2649 "title": "Provider degraded",
2650 "description": "Requests may take longer than usual.",
2651 "_meta": { "source": "fallback" }
2652 });
2653 assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
2654
2655 let v1_notice = crate::v1::SessionUpdate::Notice(
2656 crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
2657 .description("Requests may take longer than usual.")
2658 .meta(meta),
2659 );
2660 assert_eq!(
2661 serde_json::to_value(v2_notice).unwrap(),
2662 serde_json::to_value(v1_notice).unwrap()
2663 );
2664
2665 let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
2666 "sessionUpdate": "notice",
2667 "severity": "critical",
2668 "title": "Provider degraded",
2669 "description": null,
2670 "_meta": null
2671 }))
2672 .unwrap() else {
2673 panic!("expected notice");
2674 };
2675
2676 assert_eq!(
2677 notice.severity,
2678 NoticeSeverity::Other("critical".to_string())
2679 );
2680 assert_eq!(notice.description, None);
2681 assert_eq!(notice.meta, None);
2682 assert_eq!(
2683 serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
2684 json!({
2685 "sessionUpdate": "notice",
2686 "severity": "critical",
2687 "title": "Provider degraded"
2688 })
2689 );
2690 }
2691
2692 #[cfg(feature = "unstable_session_notices")]
2693 #[test]
2694 fn malformed_known_notice_is_not_hidden_as_unknown() {
2695 use serde_json::json;
2696
2697 for malformed in [
2698 json!({
2699 "sessionUpdate": "notice",
2700 "severity": "warning"
2701 }),
2702 json!({
2703 "sessionUpdate": "notice",
2704 "severity": "warning",
2705 "title": null
2706 }),
2707 json!({
2708 "sessionUpdate": "notice",
2709 "title": "MCP server unavailable"
2710 }),
2711 json!({
2712 "sessionUpdate": "notice",
2713 "severity": null,
2714 "title": "MCP server unavailable"
2715 }),
2716 ] {
2717 assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
2718 }
2719 }
2720
2721 #[cfg(not(feature = "unstable_session_notices"))]
2722 #[test]
2723 fn unsupported_notice_is_preserved_as_an_unknown_update() {
2724 use serde_json::json;
2725
2726 let SessionUpdate::Other(notice) = serde_json::from_value(json!({
2727 "sessionUpdate": "notice",
2728 "severity": "warning",
2729 "title": "MCP server unavailable"
2730 }))
2731 .unwrap() else {
2732 panic!("expected unknown session update");
2733 };
2734
2735 assert_eq!(notice.session_update, "notice");
2736 assert_eq!(notice.fields.get("severity"), Some(&json!("warning")));
2737 assert_eq!(
2738 notice.fields.get("title"),
2739 Some(&json!("MCP server unavailable"))
2740 );
2741 }
2742
2743 #[cfg(feature = "unstable_session_compaction")]
2744 #[test]
2745 fn compaction_updates_preserve_patch_and_open_status_semantics() {
2746 use serde_json::json;
2747
2748 assert_eq!(
2749 serde_json::to_value(SessionUpdate::CompactionUpdate(
2750 CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
2751 ContentBlock::Text(crate::v2::TextContent::new("retained")),
2752 ]),
2753 ))
2754 .unwrap(),
2755 json!({
2756 "sessionUpdate": "compaction_update",
2757 "compactionId": "cmp_001",
2758 "status": "completed",
2759 "summary": [{ "type": "text", "text": "retained" }]
2760 })
2761 );
2762
2763 let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
2764 "sessionUpdate": "compaction_update",
2765 "compactionId": "cmp_001",
2766 "status": "paused",
2767 "summary": null
2768 }))
2769 .unwrap() else {
2770 panic!("expected compaction update");
2771 };
2772 assert_eq!(update.status, CompactionStatus::Other("paused".into()));
2773 assert!(update.summary.is_null());
2774 assert!(update.error.is_undefined());
2775 }
2776
2777 #[cfg(feature = "unstable_session_compaction")]
2778 #[test]
2779 fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
2780 use serde_json::json;
2781
2782 assert!(
2783 serde_json::from_value::<SessionUpdate>(json!({
2784 "sessionUpdate": "compaction_update",
2785 "status": "completed"
2786 }))
2787 .is_err()
2788 );
2789 assert!(
2790 serde_json::from_value::<SessionUpdate>(json!({
2791 "sessionUpdate": "compaction_summary_chunk",
2792 "compactionId": "cmp_001"
2793 }))
2794 .is_err()
2795 );
2796 }
2797
2798 #[test]
2799 fn test_elicitation_capability_semantics() {
2800 use serde_json::json;
2801
2802 let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
2803 assert!(unsupported.elicitation.is_none());
2804
2805 let null: ClientCapabilities =
2806 serde_json::from_value(json!({ "elicitation": null })).unwrap();
2807 assert!(null.elicitation.is_none());
2808
2809 let malformed: ClientCapabilities =
2810 serde_json::from_value(json!({ "elicitation": false })).unwrap();
2811 assert!(malformed.elicitation.is_none());
2812
2813 let empty: ClientCapabilities =
2814 serde_json::from_value(json!({ "elicitation": {} })).unwrap();
2815 let empty = empty.elicitation.expect("present capability");
2816 assert!(!empty.supports_form());
2817 assert!(!empty.supports_url());
2818
2819 let form_only: ClientCapabilities = serde_json::from_value(json!({
2820 "elicitation": { "form": {} }
2821 }))
2822 .unwrap();
2823 let form_only = form_only.elicitation.expect("advertised capability");
2824 assert!(form_only.supports_form());
2825 assert!(!form_only.supports_url());
2826
2827 let url_only: ClientCapabilities = serde_json::from_value(json!({
2828 "elicitation": { "url": {} }
2829 }))
2830 .unwrap();
2831 let url_only = url_only.elicitation.expect("advertised capability");
2832 assert!(!url_only.supports_form());
2833 assert!(url_only.supports_url());
2834
2835 let both: ClientCapabilities = serde_json::from_value(json!({
2836 "elicitation": { "form": {}, "url": {} }
2837 }))
2838 .unwrap();
2839 let both = both.elicitation.expect("advertised capability");
2840 assert!(both.supports_form());
2841 assert!(both.supports_url());
2842 }
2843
2844 #[test]
2845 fn test_elicitation_method_routing_and_envelopes() {
2846 use serde_json::json;
2847
2848 assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
2849 assert_eq!(
2850 CLIENT_METHOD_NAMES.elicitation_complete,
2851 "elicitation/complete"
2852 );
2853
2854 let request =
2855 AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
2856 crate::v2::ElicitationFormMode::new(
2857 crate::v2::ElicitationSessionScope::new("sess_1"),
2858 crate::v2::ElicitationSchema::new(),
2859 ),
2860 "Choose a value",
2861 )));
2862 assert_eq!(request.method(), "elicitation/create");
2863 let method = Arc::from(request.method());
2864 let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
2865 id: crate::v2::RequestId::Number(7),
2866 method,
2867 params: Some(request),
2868 });
2869 assert_eq!(
2870 serde_json::to_value(request).unwrap(),
2871 json!({
2872 "jsonrpc": "2.0",
2873 "id": 7,
2874 "method": "elicitation/create",
2875 "params": {
2876 "mode": "form",
2877 "sessionId": "sess_1",
2878 "message": "Choose a value",
2879 "requestedSchema": { "type": "object", "properties": {} }
2880 }
2881 })
2882 );
2883
2884 let notification = AgentNotification::CompleteElicitationNotification(Box::new(
2885 CompleteElicitationNotification::new("elic_1"),
2886 ));
2887 assert_eq!(notification.method(), "elicitation/complete");
2888 let method = Arc::from(notification.method());
2889 let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
2890 method,
2891 params: Some(notification),
2892 });
2893 assert_eq!(
2894 serde_json::to_value(notification).unwrap(),
2895 json!({
2896 "jsonrpc": "2.0",
2897 "method": "elicitation/complete",
2898 "params": { "elicitationId": "elic_1" }
2899 })
2900 );
2901 }
2902
2903 #[test]
2904 fn test_client_capabilities_auth_defaults_on_malformed_value() {
2905 use serde_json::json;
2906
2907 let capabilities: ClientCapabilities = serde_json::from_value(json!({
2908 "auth": false
2909 }))
2910 .unwrap();
2911
2912 assert_eq!(capabilities.auth, None);
2913 }
2914
2915 #[test]
2916 fn test_serialization_behavior() {
2917 use serde_json::json;
2918
2919 assert_eq!(
2920 serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
2921 SessionInfoUpdate {
2922 title: MaybeUndefined::Undefined,
2923 updated_at: MaybeUndefined::Undefined,
2924 meta: MaybeUndefined::Undefined
2925 }
2926 );
2927 assert_eq!(
2928 serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
2929 .unwrap(),
2930 SessionInfoUpdate {
2931 title: MaybeUndefined::Null,
2932 updated_at: MaybeUndefined::Null,
2933 meta: MaybeUndefined::Undefined
2934 }
2935 );
2936 assert_eq!(
2937 serde_json::from_value::<SessionInfoUpdate>(
2938 json!({"title": "title", "updatedAt": "timestamp"})
2939 )
2940 .unwrap(),
2941 SessionInfoUpdate {
2942 title: MaybeUndefined::Value("title".to_string()),
2943 updated_at: MaybeUndefined::Value("timestamp".to_string()),
2944 meta: MaybeUndefined::Undefined
2945 }
2946 );
2947
2948 let clear_meta =
2949 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
2950 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
2951
2952 let mut meta = Meta::new();
2953 meta.insert("source".to_string(), json!("session-info"));
2954
2955 assert_eq!(
2956 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
2957 "source": "session-info"
2958 }}))
2959 .unwrap()
2960 .meta,
2961 MaybeUndefined::Value(meta.clone())
2962 );
2963
2964 assert_eq!(
2965 serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
2966 json!({})
2967 );
2968
2969 assert_eq!(
2970 serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
2971 json!({"_meta": null})
2972 );
2973
2974 assert_eq!(
2975 serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
2976 json!({"_meta": {
2977 "source": "session-info"
2978 }})
2979 );
2980 assert_eq!(
2981 serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
2982 json!({"title": "title"})
2983 );
2984 assert_eq!(
2985 serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
2986 json!({"title": null})
2987 );
2988 assert_eq!(
2989 serde_json::to_value(
2990 SessionInfoUpdate::new()
2991 .title("title")
2992 .title(MaybeUndefined::Undefined)
2993 )
2994 .unwrap(),
2995 json!({})
2996 );
2997 }
2998
2999 #[test]
3000 fn test_content_chunk_message_id_serialization() {
3001 use serde_json::json;
3002
3003 assert_eq!(
3004 serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
3005 ContentBlock::Text(crate::v2::TextContent::new("Hello")),
3006 "msg_agent_c42b9",
3007 )))
3008 .unwrap(),
3009 json!({
3010 "sessionUpdate": "agent_message_chunk",
3011 "messageId": "msg_agent_c42b9",
3012 "content": {
3013 "type": "text",
3014 "text": "Hello"
3015 }
3016 })
3017 );
3018
3019 let err = serde_json::from_value::<ContentChunk>(json!({
3020 "content": {
3021 "type": "text",
3022 "text": "Hello"
3023 }
3024 }))
3025 .unwrap_err();
3026
3027 assert!(err.to_string().contains("messageId"), "{err}");
3028 }
3029
3030 #[test]
3031 fn test_tool_call_content_chunk_serialization() {
3032 use serde_json::json;
3033
3034 assert_eq!(
3035 serde_json::to_value(SessionUpdate::ToolCallContentChunk(
3036 ToolCallContentChunk::new(
3037 "call_001",
3038 crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
3039 )
3040 ))
3041 .unwrap(),
3042 json!({
3043 "sessionUpdate": "tool_call_content_chunk",
3044 "toolCallId": "call_001",
3045 "content": {
3046 "type": "content",
3047 "content": {
3048 "type": "text",
3049 "text": "partial output"
3050 }
3051 }
3052 })
3053 );
3054
3055 let err = serde_json::from_value::<ToolCallContentChunk>(json!({
3056 "content": {
3057 "type": "content",
3058 "content": {
3059 "type": "text",
3060 "text": "partial output"
3061 }
3062 }
3063 }))
3064 .unwrap_err();
3065
3066 assert!(err.to_string().contains("toolCallId"), "{err}");
3067 }
3068
3069 #[test]
3070 fn test_full_message_serialization() {
3071 use serde_json::json;
3072
3073 assert_eq!(
3074 serde_json::to_value(SessionUpdate::UserMessage(
3075 UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
3076 crate::v2::TextContent::new("Hello")
3077 )])
3078 ))
3079 .unwrap(),
3080 json!({
3081 "sessionUpdate": "user_message",
3082 "messageId": "msg_user_8f7a1",
3083 "content": [
3084 {
3085 "type": "text",
3086 "text": "Hello"
3087 }
3088 ]
3089 })
3090 );
3091
3092 assert_eq!(
3093 serde_json::to_value(SessionUpdate::AgentMessage(
3094 AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
3095 crate::v2::TextContent::new("Hello")
3096 )])
3097 ))
3098 .unwrap(),
3099 json!({
3100 "sessionUpdate": "agent_message",
3101 "messageId": "msg_agent_c42b9",
3102 "content": [
3103 {
3104 "type": "text",
3105 "text": "Hello"
3106 }
3107 ]
3108 })
3109 );
3110
3111 assert_eq!(
3112 serde_json::to_value(SessionUpdate::AgentThought(
3113 AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
3114 crate::v2::TextContent::new("Need to inspect the call sites first.")
3115 )])
3116 ))
3117 .unwrap(),
3118 json!({
3119 "sessionUpdate": "agent_thought",
3120 "messageId": "msg_thought_a12",
3121 "content": [
3122 {
3123 "type": "text",
3124 "text": "Need to inspect the call sites first."
3125 }
3126 ]
3127 })
3128 );
3129 }
3130
3131 #[test]
3132 fn test_message_upsert_serialization() {
3133 use serde_json::json;
3134
3135 assert_eq!(
3136 serde_json::to_value(SessionUpdate::UserMessage(
3137 UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
3138 ))
3139 .unwrap(),
3140 json!({
3141 "sessionUpdate": "user_message",
3142 "messageId": "msg_empty",
3143 "content": []
3144 })
3145 );
3146
3147 let empty = serde_json::from_value::<UserMessage>(json!({
3148 "messageId": "msg_empty",
3149 "content": []
3150 }))
3151 .unwrap();
3152 assert!(matches!(
3153 empty.content,
3154 MaybeUndefined::Value(ref content) if content.is_empty()
3155 ));
3156
3157 let patch = serde_json::from_value::<AgentMessage>(json!({
3158 "messageId": "msg_agent_c42b9"
3159 }))
3160 .unwrap();
3161 assert_eq!(patch.content, MaybeUndefined::Undefined);
3162 assert_eq!(patch.meta, MaybeUndefined::Undefined);
3163
3164 let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
3165 "messageId": "msg_agent_c42b9",
3166 "_meta": false
3167 }))
3168 .unwrap();
3169 assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
3170
3171 let patch = serde_json::from_value::<AgentThought>(json!({
3172 "messageId": "msg_thought_a12"
3173 }))
3174 .unwrap();
3175 assert_eq!(patch.content, MaybeUndefined::Undefined);
3176
3177 let clear = serde_json::from_value::<UserMessage>(json!({
3178 "messageId": "msg_user_8f7a1",
3179 "content": null
3180 }))
3181 .unwrap();
3182 assert_eq!(clear.content, MaybeUndefined::Null);
3183
3184 let clear_meta = serde_json::from_value::<UserMessage>(json!({
3185 "messageId": "msg_user_8f7a1",
3186 "_meta": null
3187 }))
3188 .unwrap();
3189 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
3190
3191 let mut meta = Meta::new();
3192 meta.insert("source".to_string(), json!("replay"));
3193
3194 assert_eq!(
3195 serde_json::to_value(SessionUpdate::UserMessage(
3196 UserMessage::new("msg_user_8f7a1").meta(meta)
3197 ))
3198 .unwrap(),
3199 json!({
3200 "sessionUpdate": "user_message",
3201 "messageId": "msg_user_8f7a1",
3202 "_meta": {
3203 "source": "replay"
3204 }
3205 })
3206 );
3207
3208 assert_eq!(
3209 serde_json::to_value(SessionUpdate::UserMessage(
3210 UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
3211 ))
3212 .unwrap(),
3213 json!({
3214 "sessionUpdate": "user_message",
3215 "messageId": "msg_user_8f7a1",
3216 "_meta": null
3217 })
3218 );
3219 }
3220
3221 #[test]
3222 fn test_usage_update_serialization() {
3223 use serde_json::json;
3224
3225 assert_eq!(
3226 serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
3227 53_000, 200_000
3228 )))
3229 .unwrap(),
3230 json!({
3231 "sessionUpdate": "usage_update",
3232 "used": 53000,
3233 "size": 200_000
3234 })
3235 );
3236
3237 assert_eq!(
3238 serde_json::to_value(SessionUpdate::UsageUpdate(
3239 UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
3240 ))
3241 .unwrap(),
3242 json!({
3243 "sessionUpdate": "usage_update",
3244 "used": 53000,
3245 "size": 200_000,
3246 "cost": {
3247 "amount": 0.045,
3248 "currency": "USD"
3249 }
3250 })
3251 );
3252
3253 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
3254 "sessionUpdate": "usage_update",
3255 "used": 53000,
3256 "size": 200_000,
3257 "cost": null
3258 }))
3259 .unwrap() else {
3260 panic!("expected usage update");
3261 };
3262
3263 assert_eq!(update.cost, None);
3264 }
3265
3266 #[test]
3267 fn test_state_update_serialization() {
3268 use serde_json::json;
3269
3270 assert_eq!(
3271 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
3272 RunningStateUpdate::new()
3273 )))
3274 .unwrap(),
3275 json!({
3276 "sessionUpdate": "state_update",
3277 "state": "running"
3278 })
3279 );
3280
3281 assert_eq!(
3282 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
3283 IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
3284 )))
3285 .unwrap(),
3286 json!({
3287 "sessionUpdate": "state_update",
3288 "state": "idle",
3289 "stopReason": "end_turn"
3290 })
3291 );
3292
3293 let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
3294 "sessionUpdate": "state_update",
3295 "state": "requires_action"
3296 }))
3297 .unwrap() else {
3298 panic!("expected state update");
3299 };
3300
3301 assert!(matches!(update, StateUpdate::RequiresAction(_)));
3302
3303 let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
3304 "sessionUpdate": "state_update",
3305 "state": "idle",
3306 "stopReason": null
3307 }))
3308 .unwrap() else {
3309 panic!("expected idle state update");
3310 };
3311
3312 assert_eq!(update.stop_reason, None);
3313
3314 let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
3315 serde_json::from_value(json!({
3316 "sessionUpdate": "state_update",
3317 "state": "_paused",
3318 "label": "Paused"
3319 }))
3320 .unwrap()
3321 else {
3322 panic!("expected unknown state update");
3323 };
3324
3325 assert_eq!(update.state, "_paused");
3326 assert_eq!(update.fields["label"], json!("Paused"));
3327 }
3328
3329 #[test]
3330 fn session_update_preserves_unknown_variant() {
3331 use serde_json::json;
3332
3333 let update: SessionUpdate = serde_json::from_value(json!({
3334 "sessionUpdate": "_status_badge",
3335 "label": "Indexing",
3336 "progress": 0.5
3337 }))
3338 .unwrap();
3339
3340 let SessionUpdate::Other(unknown) = update else {
3341 panic!("expected unknown session update");
3342 };
3343
3344 assert_eq!(unknown.session_update, "_status_badge");
3345 assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
3346 assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
3347
3348 assert_eq!(
3349 serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
3350 json!({
3351 "sessionUpdate": "_status_badge",
3352 "label": "Indexing",
3353 "progress": 0.5
3354 })
3355 );
3356 }
3357
3358 #[test]
3359 fn terminal_session_updates_use_known_discriminators() {
3360 use serde_json::json;
3361
3362 assert_eq!(
3363 serde_json::to_value(SessionUpdate::TerminalUpdate(
3364 TerminalUpdate::new("term_1").command("cargo test")
3365 ))
3366 .unwrap(),
3367 json!({
3368 "sessionUpdate": "terminal_update",
3369 "terminalId": "term_1",
3370 "command": "cargo test"
3371 })
3372 );
3373 assert_eq!(
3374 serde_json::to_value(SessionUpdate::TerminalOutputChunk(
3375 TerminalOutputChunk::new("term_1", "dGVzdAo=")
3376 ))
3377 .unwrap(),
3378 json!({
3379 "sessionUpdate": "terminal_output_chunk",
3380 "terminalId": "term_1",
3381 "data": "dGVzdAo="
3382 })
3383 );
3384 }
3385
3386 #[test]
3387 fn session_update_does_not_hide_malformed_known_terminal_variants() {
3388 use serde_json::json;
3389
3390 assert!(
3391 serde_json::from_value::<SessionUpdate>(json!({
3392 "sessionUpdate": "terminal_update"
3393 }))
3394 .is_err()
3395 );
3396 assert!(
3397 serde_json::from_value::<SessionUpdate>(json!({
3398 "sessionUpdate": "terminal_output_chunk",
3399 "terminalId": "term_1"
3400 }))
3401 .is_err()
3402 );
3403 }
3404
3405 #[test]
3406 fn test_plan_update_serialization() {
3407 use serde_json::json;
3408
3409 let plan_update =
3410 SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
3411 "plan-1",
3412 vec![crate::v2::PlanEntry::new(
3413 "Step 1",
3414 crate::v2::PlanEntryPriority::High,
3415 crate::v2::PlanEntryStatus::Pending,
3416 )],
3417 )));
3418
3419 assert_eq!(
3420 serde_json::to_value(plan_update).unwrap(),
3421 json!({
3422 "sessionUpdate": "plan_update",
3423 "plan": {
3424 "type": "items",
3425 "planId": "plan-1",
3426 "entries": [
3427 {
3428 "content": "Step 1",
3429 "priority": "high",
3430 "status": "pending"
3431 }
3432 ]
3433 }
3434 })
3435 );
3436 }
3437
3438 #[cfg(feature = "unstable_plan_operations")]
3439 #[test]
3440 fn test_plan_removed_serialization() {
3441 use serde_json::json;
3442
3443 assert_eq!(
3444 serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
3445 json!({
3446 "sessionUpdate": "plan_removed",
3447 "planId": "plan-1"
3448 })
3449 );
3450 }
3451
3452 #[test]
3453 fn available_command_input_preserves_unknown_typed_variant() {
3454 use serde_json::json;
3455
3456 let input: AvailableCommandInput = serde_json::from_value(json!({
3457 "type": "_choices",
3458 "hint": "Pick one",
3459 "options": ["fast", "careful"]
3460 }))
3461 .unwrap();
3462
3463 let AvailableCommandInput::Other(unknown) = input else {
3464 panic!("expected unknown command input");
3465 };
3466
3467 assert_eq!(unknown.type_, "_choices");
3468 assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
3469 assert_eq!(
3470 unknown.fields.get("options"),
3471 Some(&json!(["fast", "careful"]))
3472 );
3473 assert_eq!(
3474 serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
3475 json!({
3476 "type": "_choices",
3477 "hint": "Pick one",
3478 "options": ["fast", "careful"]
3479 })
3480 );
3481 }
3482
3483 #[test]
3484 fn available_command_input_text_uses_type_discriminator() {
3485 use serde_json::json;
3486
3487 let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
3488
3489 let json = serde_json::to_value(&input).unwrap();
3490 assert_eq!(
3491 json,
3492 json!({
3493 "type": "text",
3494 "hint": "Describe changes"
3495 })
3496 );
3497
3498 let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
3499 assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
3500 }
3501
3502 #[test]
3503 fn request_permission_subject_tool_call_uses_type_discriminator() {
3504 use serde_json::json;
3505
3506 let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
3507
3508 let json = serde_json::to_value(&subject).unwrap();
3509 assert_eq!(
3510 json,
3511 json!({
3512 "type": "tool_call",
3513 "toolCall": {
3514 "toolCallId": "call_001"
3515 }
3516 })
3517 );
3518
3519 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
3520 assert!(matches!(
3521 roundtripped,
3522 RequestPermissionSubject::ToolCall(_)
3523 ));
3524 }
3525
3526 #[test]
3527 fn request_permission_subject_command_uses_type_discriminator() {
3528 use serde_json::json;
3529
3530 let mut meta = Meta::new();
3531 meta.insert("source".to_string(), json!("shell"));
3532 let subject = RequestPermissionSubject::from(
3533 CommandPermissionSubject::new("cargo test", "/workspace/project")
3534 .tool_call_id("call_001")
3535 .terminal_id("term_1")
3536 .meta(meta),
3537 );
3538
3539 let json = serde_json::to_value(&subject).unwrap();
3540 assert_eq!(
3541 json,
3542 json!({
3543 "type": "command",
3544 "command": "cargo test",
3545 "cwd": "/workspace/project",
3546 "toolCallId": "call_001",
3547 "terminalId": "term_1",
3548 "_meta": {
3549 "source": "shell"
3550 }
3551 })
3552 );
3553
3554 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
3555 assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
3556 }
3557
3558 #[test]
3559 fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
3560 use serde_json::json;
3561
3562 let subject: RequestPermissionSubject = serde_json::from_value(json!({
3563 "type": "command",
3564 "command": "cargo test",
3565 "cwd": "/workspace/project",
3566 "toolCallId": null,
3567 "terminalId": null,
3568 "_meta": null
3569 }))
3570 .unwrap();
3571
3572 let RequestPermissionSubject::Command(subject) = subject else {
3573 panic!("expected command permission subject");
3574 };
3575 assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
3576 assert_eq!(subject.tool_call_id, None);
3577 assert_eq!(subject.terminal_id, None);
3578 assert_eq!(subject.meta, None);
3579 assert_eq!(
3580 serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
3581 json!({
3582 "type": "command",
3583 "command": "cargo test",
3584 "cwd": "/workspace/project"
3585 })
3586 );
3587 }
3588
3589 #[test]
3590 fn request_permission_subject_preserves_unknown_variant() {
3591 use serde_json::json;
3592
3593 let subject: RequestPermissionSubject = serde_json::from_value(json!({
3594 "type": "_review",
3595 "reason": "needs-review",
3596 "retryAfterSeconds": 30
3597 }))
3598 .unwrap();
3599
3600 let RequestPermissionSubject::Other(unknown) = subject else {
3601 panic!("expected unknown permission subject");
3602 };
3603
3604 assert_eq!(unknown.type_, "_review");
3605 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
3606 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
3607 assert_eq!(
3608 serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
3609 json!({
3610 "type": "_review",
3611 "reason": "needs-review",
3612 "retryAfterSeconds": 30
3613 })
3614 );
3615 }
3616
3617 #[test]
3618 fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
3619 use serde_json::json;
3620
3621 assert!(
3622 serde_json::from_value::<RequestPermissionSubject>(json!({
3623 "type": "tool_call"
3624 }))
3625 .is_err()
3626 );
3627 assert!(
3628 serde_json::from_value::<RequestPermissionSubject>(json!({
3629 "type": 1
3630 }))
3631 .is_err()
3632 );
3633 assert!(
3634 serde_json::from_value::<RequestPermissionSubject>(json!({
3635 "type": "command",
3636 "cwd": "/workspace/project"
3637 }))
3638 .is_err()
3639 );
3640 assert!(
3641 serde_json::from_value::<RequestPermissionSubject>(json!({
3642 "type": "command",
3643 "command": "cargo test"
3644 }))
3645 .is_err()
3646 );
3647 assert!(
3648 serde_json::from_value::<RequestPermissionSubject>(json!({
3649 "type": "command",
3650 "command": "cargo test",
3651 "cwd": null
3652 }))
3653 .is_err()
3654 );
3655 }
3656
3657 #[test]
3658 fn request_permission_title_and_description_are_separate_from_tool_call_content() {
3659 use serde_json::json;
3660
3661 let request =
3662 RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
3663 .description("Allow this tool to edit src/main.rs?")
3664 .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
3665 "call_001",
3666 )));
3667
3668 assert_eq!(
3669 serde_json::to_value(request).unwrap(),
3670 json!({
3671 "sessionId": "sess_abc123def456",
3672 "title": "Approve file edit?",
3673 "description": "Allow this tool to edit src/main.rs?",
3674 "subject": {
3675 "type": "tool_call",
3676 "toolCall": {
3677 "toolCallId": "call_001"
3678 }
3679 },
3680 "options": []
3681 })
3682 );
3683 }
3684
3685 #[test]
3686 fn request_permission_requires_title_and_allows_missing_subject() {
3687 use serde_json::json;
3688
3689 let request = RequestPermissionRequest::new(
3690 "sess_abc123def456",
3691 "Approve elevated permissions?",
3692 Vec::new(),
3693 );
3694
3695 assert_eq!(
3696 serde_json::to_value(request).unwrap(),
3697 json!({
3698 "sessionId": "sess_abc123def456",
3699 "title": "Approve elevated permissions?",
3700 "options": []
3701 })
3702 );
3703
3704 let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
3705 "sessionId": "sess_abc123def456",
3706 "title": "Approve elevated permissions?",
3707 "options": []
3708 }))
3709 .unwrap();
3710 assert!(missing_subject.subject.is_none());
3711
3712 let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
3713 "sessionId": "sess_abc123def456",
3714 "title": "Approve elevated permissions?",
3715 "subject": null,
3716 "options": []
3717 }))
3718 .unwrap();
3719 assert!(null_subject.subject.is_none());
3720
3721 assert!(
3722 serde_json::from_value::<RequestPermissionRequest>(json!({
3723 "sessionId": "sess_abc123def456",
3724 "options": []
3725 }))
3726 .is_err()
3727 );
3728 }
3729
3730 #[test]
3731 fn request_permission_outcome_preserves_unknown_variant() {
3732 use serde_json::json;
3733
3734 let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
3735 "outcome": "_defer",
3736 "reason": "needs-review",
3737 "retryAfterSeconds": 30
3738 }))
3739 .unwrap();
3740
3741 let RequestPermissionOutcome::Other(unknown) = outcome else {
3742 panic!("expected unknown permission outcome");
3743 };
3744
3745 assert_eq!(unknown.outcome, "_defer");
3746 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
3747 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
3748 assert_eq!(
3749 serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
3750 json!({
3751 "outcome": "_defer",
3752 "reason": "needs-review",
3753 "retryAfterSeconds": 30
3754 })
3755 );
3756 }
3757
3758 #[test]
3759 fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
3760 use serde_json::json;
3761
3762 assert!(
3763 serde_json::from_value::<RequestPermissionOutcome>(json!({
3764 "outcome": "selected"
3765 }))
3766 .is_err()
3767 );
3768 assert!(
3769 serde_json::from_value::<RequestPermissionOutcome>(json!({
3770 "outcome": 1
3771 }))
3772 .is_err()
3773 );
3774 }
3775
3776 #[test]
3777 fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
3778 use serde_json::json;
3779
3780 assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
3781 assert!(
3782 serde_json::from_value::<AvailableCommandInput>(json!({
3783 "hint": "Pick one"
3784 }))
3785 .is_err()
3786 );
3787 assert!(
3788 serde_json::from_value::<AvailableCommandInput>(json!({
3789 "type": 1,
3790 "hint": "Pick one"
3791 }))
3792 .is_err()
3793 );
3794 assert!(
3795 serde_json::from_value::<OtherAvailableCommandInput>(json!({
3796 "type": "text",
3797 "hint": "Pick one"
3798 }))
3799 .is_err()
3800 );
3801 }
3802
3803 #[cfg(feature = "unstable_nes")]
3804 #[test]
3805 fn test_client_capabilities_position_encodings_serialization() {
3806 use serde_json::json;
3807
3808 let capabilities = ClientCapabilities::new().position_encodings(vec![
3809 PositionEncodingKind::Utf32,
3810 PositionEncodingKind::Utf16,
3811 ]);
3812 let json = serde_json::to_value(&capabilities).unwrap();
3813
3814 assert_eq!(json["positionEncodings"], json!(["utf-32", "utf-16"]));
3815 }
3816
3817 #[cfg(feature = "unstable_mcp_over_acp")]
3818 #[test]
3819 fn test_agent_mcp_request_method_names() {
3820 use serde_json::json;
3821
3822 let params: serde_json::Map<String, serde_json::Value> =
3823 [("cursor".to_string(), json!("abc"))].into_iter().collect();
3824
3825 assert_eq!(CLIENT_METHOD_NAMES.mcp_connect, "mcp/connect");
3826 assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
3827 assert_eq!(CLIENT_METHOD_NAMES.mcp_disconnect, "mcp/disconnect");
3828
3829 assert_eq!(
3830 AgentRequest::ConnectMcpRequest(Box::new(ConnectMcpRequest::new("server-1"))).method(),
3831 "mcp/connect"
3832 );
3833 assert_eq!(
3834 AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
3835 "conn-1",
3836 "tools/list"
3837 )))
3838 .method(),
3839 "mcp/message"
3840 );
3841 assert_eq!(
3842 AgentRequest::DisconnectMcpRequest(Box::new(DisconnectMcpRequest::new("conn-1")))
3843 .method(),
3844 "mcp/disconnect"
3845 );
3846 assert_eq!(
3847 AgentNotification::MessageMcpNotification(Box::new(MessageMcpNotification::new(
3848 "conn-1",
3849 "notifications/progress"
3850 )))
3851 .method(),
3852 "mcp/message"
3853 );
3854
3855 assert_eq!(
3856 serde_json::to_value(ConnectMcpRequest::new("server-1")).unwrap(),
3857 json!({ "serverId": "server-1" })
3858 );
3859 assert_eq!(
3860 serde_json::to_value(ConnectMcpResponse::new("conn-1")).unwrap(),
3861 json!({ "connectionId": "conn-1" })
3862 );
3863 assert_eq!(
3864 serde_json::to_value(MessageMcpRequest::new("conn-1", "tools/list").params(params))
3865 .unwrap(),
3866 json!({
3867 "connectionId": "conn-1",
3868 "method": "tools/list",
3869 "params": { "cursor": "abc" }
3870 })
3871 );
3872 assert_eq!(
3873 serde_json::to_value(DisconnectMcpRequest::new("conn-1")).unwrap(),
3874 json!({ "connectionId": "conn-1" })
3875 );
3876 assert_eq!(
3877 serde_json::to_value(MessageMcpNotification::new(
3878 "conn-1",
3879 "notifications/progress"
3880 ))
3881 .unwrap(),
3882 json!({
3883 "connectionId": "conn-1",
3884 "method": "notifications/progress"
3885 })
3886 );
3887
3888 let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
3889 "connectionId": "conn-1",
3890 "method": "tools/list",
3891 "params": null
3892 }))
3893 .unwrap();
3894 assert_eq!(request_with_null_params.params, None);
3895 }
3896
3897 #[test]
3898 fn test_auth_capabilities_serialize_terminal_support_as_object() {
3899 use serde_json::json;
3900
3901 let capabilities = AuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
3902
3903 assert_eq!(
3904 serde_json::to_value(&capabilities).unwrap(),
3905 json!({
3906 "terminal": {}
3907 })
3908 );
3909
3910 let deserialized: AuthCapabilities = serde_json::from_value(json!({
3911 "terminal": false
3912 }))
3913 .unwrap();
3914 assert!(deserialized.terminal.is_none());
3915 }
3916
3917 #[test]
3918 fn request_permission_request_rejects_malformed_options() {
3919 use serde_json::json;
3920
3921 assert!(
3922 serde_json::from_value::<RequestPermissionRequest>(json!({
3923 "sessionId": "sess-1",
3924 "title": "Run tool?",
3925 "options": "not-an-array"
3926 }))
3927 .is_err()
3928 );
3929 assert!(
3930 serde_json::from_value::<RequestPermissionRequest>(json!({
3931 "sessionId": "sess-1",
3932 "title": "Run tool?",
3933 "options": [{"optionId": "allow"}]
3934 }))
3935 .is_err()
3936 );
3937 }
3938
3939 #[cfg(feature = "unstable_plan_operations")]
3940 #[test]
3941 fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
3942 use serde_json::json;
3943
3944 assert!(
3945 serde_json::from_value::<SessionUpdate>(json!({
3946 "sessionUpdate": "plan_removed"
3947 }))
3948 .is_err()
3949 );
3950 }
3951}