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_auth_status")]
15use super::AuthStatusUpdateNotification;
16#[cfg(feature = "unstable_plan_operations")]
17use super::PlanRemoved;
18#[cfg(feature = "unstable_end_turn_token_usage")]
19use super::Usage;
20use super::{
21 AbsolutePath, ContentBlock, ExtNotification, ExtRequest, ExtResponse, Meta, PlanUpdate,
22 SessionConfigOption, SessionId, StopReason, TerminalId, TerminalOutputChunk, TerminalUpdate,
23 ToolCallContentChunk, ToolCallId, ToolCallUpdate,
24};
25use super::{
26 CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse,
27 ElicitationCapabilities,
28};
29use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined};
30
31#[cfg(feature = "unstable_mcp_over_acp")]
32use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse};
33
34#[serde_as]
42#[skip_serializing_none]
43#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_UPDATE_NOTIFICATION)))]
46#[serde(rename_all = "camelCase")]
47#[non_exhaustive]
48pub struct UpdateSessionNotification {
49 pub session_id: SessionId,
51 pub update: SessionUpdate,
53 #[serde_as(deserialize_as = "DefaultOnError")]
59 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
60 #[serde(default)]
61 #[serde(rename = "_meta")]
62 pub meta: Option<Meta>,
63}
64
65impl UpdateSessionNotification {
66 #[must_use]
68 pub fn new(session_id: impl Into<SessionId>, update: SessionUpdate) -> Self {
69 Self {
70 session_id: session_id.into(),
71 update,
72 meta: None,
73 }
74 }
75
76 #[must_use]
82 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
83 self.meta = meta.into_option();
84 self
85 }
86}
87
88#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
96#[non_exhaustive]
97pub enum SessionUpdate {
98 UserMessageChunk(ContentChunk),
100 UserMessage(UserMessage),
106 AgentMessageChunk(ContentChunk),
108 AgentMessage(AgentMessage),
114 AgentThoughtChunk(ContentChunk),
116 AgentThought(AgentThought),
122 StateUpdate(StateUpdate),
124 ToolCallContentChunk(ToolCallContentChunk),
126 ToolCallUpdate(ToolCallUpdate),
128 TerminalUpdate(TerminalUpdate),
130 TerminalOutputChunk(TerminalOutputChunk),
132 PlanUpdate(PlanUpdate),
135 #[cfg(feature = "unstable_plan_operations")]
141 PlanRemoved(PlanRemoved),
142 AvailableCommandsUpdate(AvailableCommandsUpdate),
144 ConfigOptionUpdate(ConfigOptionUpdate),
146 SessionInfoUpdate(SessionInfoUpdate),
148 UsageUpdate(UsageUpdate),
150 Notice(Notice),
155 CompactionUpdate(CompactionUpdate),
157 CompactionSummaryChunk(CompactionSummaryChunk),
159 #[cfg(feature = "unstable_subagents")]
166 SubagentUpdate(SubagentUpdate),
167 #[cfg(feature = "unstable_subagents")]
174 SessionMessage(SessionMessage),
175 #[cfg(feature = "unstable_subagents")]
181 SessionMessageChunk(SessionMessageChunk),
182 #[serde(untagged)]
192 Other(OtherSessionUpdate),
193}
194
195#[cfg(feature = "unstable_subagents")]
204#[serde_as]
205#[skip_serializing_none]
206#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
208#[serde(rename_all = "camelCase")]
209#[non_exhaustive]
210pub struct SessionMessageChunk {
211 pub message_id: MessageId,
213 pub content: ContentBlock,
215 #[serde_as(deserialize_as = "DefaultOnError")]
222 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
223 #[serde(default, rename = "_meta")]
224 pub meta: Option<Meta>,
225}
226
227#[cfg(feature = "unstable_subagents")]
228impl SessionMessageChunk {
229 #[must_use]
231 pub fn new(message_id: impl Into<MessageId>, content: ContentBlock) -> Self {
232 Self {
233 message_id: message_id.into(),
234 content,
235 meta: None,
236 }
237 }
238
239 #[must_use]
241 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
242 self.meta = meta.into_option();
243 self
244 }
245}
246
247#[cfg(feature = "unstable_subagents")]
253#[serde_as]
254#[skip_serializing_none]
255#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
257#[serde(rename_all = "camelCase")]
258#[non_exhaustive]
259pub struct SessionMessage {
260 pub message_id: MessageId,
262 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
266 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
267 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
268 pub sender_session_id: MaybeUndefined<SessionId>,
269 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
273 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
274 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
275 pub recipient_session_id: MaybeUndefined<SessionId>,
276 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
282 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
283 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
284 pub sender_message_id: MaybeUndefined<MessageId>,
285 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
291 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
292 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
293 pub recipient_message_id: MaybeUndefined<MessageId>,
294 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
297 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
298 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
299 pub content: MaybeUndefined<Vec<ContentBlock>>,
300 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
309 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
310 #[serde(
311 default,
312 rename = "_meta",
313 skip_serializing_if = "MaybeUndefined::is_undefined"
314 )]
315 pub meta: MaybeUndefined<Meta>,
316}
317
318#[cfg(feature = "unstable_subagents")]
319impl SessionMessage {
320 #[must_use]
322 pub fn new(message_id: impl Into<MessageId>) -> Self {
323 Self {
324 message_id: message_id.into(),
325 sender_session_id: MaybeUndefined::Undefined,
326 recipient_session_id: MaybeUndefined::Undefined,
327 sender_message_id: MaybeUndefined::Undefined,
328 recipient_message_id: MaybeUndefined::Undefined,
329 content: MaybeUndefined::Undefined,
330 meta: MaybeUndefined::Undefined,
331 }
332 }
333
334 #[must_use]
336 pub fn sender_session_id(
337 mut self,
338 sender_session_id: impl IntoMaybeUndefined<SessionId>,
339 ) -> Self {
340 self.sender_session_id = sender_session_id.into_maybe_undefined();
341 self
342 }
343
344 #[must_use]
346 pub fn recipient_session_id(
347 mut self,
348 recipient_session_id: impl IntoMaybeUndefined<SessionId>,
349 ) -> Self {
350 self.recipient_session_id = recipient_session_id.into_maybe_undefined();
351 self
352 }
353
354 #[must_use]
356 pub fn sender_message_id(
357 mut self,
358 sender_message_id: impl IntoMaybeUndefined<MessageId>,
359 ) -> Self {
360 self.sender_message_id = sender_message_id.into_maybe_undefined();
361 self
362 }
363
364 #[must_use]
366 pub fn recipient_message_id(
367 mut self,
368 recipient_message_id: impl IntoMaybeUndefined<MessageId>,
369 ) -> Self {
370 self.recipient_message_id = recipient_message_id.into_maybe_undefined();
371 self
372 }
373
374 #[must_use]
376 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
377 self.content = content.into_maybe_undefined();
378 self
379 }
380
381 #[must_use]
386 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
387 self.meta = meta.into_maybe_undefined();
388 self
389 }
390}
391
392#[cfg(all(test, feature = "unstable_subagents"))]
393mod session_message_tests {
394 use super::*;
395 use serde_json::{Value, json};
396
397 #[test]
398 fn transcript_envelopes_and_multimodal_content() {
399 for (transcript, sender, recipient, id) in [
400 ("parent", "parent", "child", "sent-1"),
401 ("child", "parent", "child", "received-9"),
402 ("child", "child", "parent", "sent-2"),
403 ] {
404 let wire = json!({"sessionId": transcript, "update": {
405 "sessionUpdate": "session_message", "messageId": id,
406 "senderSessionId": sender, "recipientSessionId": recipient, "content": [
407 {"type": "text", "text": "hello"},
408 {"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"}
409 ]
410 }});
411 let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
412 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
413 }
414 }
415
416 #[test]
417 fn chunks_and_upserts_share_transcript_local_identity() {
418 for (transcript, id) in [("parent", "sent-1"), ("child", "received-9")] {
419 let wire = json!({"sessionId": transcript, "update": {
420 "sessionUpdate": "session_message_chunk", "messageId": id,
421 "content": {"type": "text", "text": "hello"}
422 }});
423 let decoded: UpdateSessionNotification = serde_json::from_value(wire.clone()).unwrap();
424 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
425 let upsert = SessionMessage::new(id)
426 .sender_session_id(SessionId::new("parent"))
427 .recipient_session_id(SessionId::new("child"))
428 .content(vec![]);
429 let chunk = SessionMessageChunk::new(
430 id,
431 ContentBlock::Text(crate::v2::TextContent::new("hello")),
432 );
433 assert_eq!(chunk.message_id, upsert.message_id);
434 assert_eq!(
435 serde_json::to_value(SessionUpdate::SessionMessageChunk(chunk)).unwrap(),
436 wire["update"]
437 );
438 assert_eq!(serde_json::to_value(upsert).unwrap()["content"], json!([]));
439 }
440 let with_identity = json!({"sessionUpdate": "session_message_chunk", "messageId": "m1",
442 "senderSessionId": "parent", "recipientSessionId": "child",
443 "senderMessageId": "sent-1", "recipientMessageId": "received-1",
444 "content": {"type": "text", "text": "hello"}});
445 let decoded: SessionUpdate = serde_json::from_value(with_identity).unwrap();
446 assert_eq!(
447 serde_json::to_value(decoded).unwrap(),
448 json!({"sessionUpdate": "session_message_chunk", "messageId": "m1",
449 "content": {"type": "text", "text": "hello"}})
450 );
451 }
452
453 #[test]
454 fn required_ids_and_chunk_content_with_upsert_patch_semantics() {
455 for (kind, content) in [
456 ("session_message", json!([])),
457 (
458 "session_message_chunk",
459 json!({"type": "text", "text": "hello"}),
460 ),
461 ] {
462 let mut base = json!({"sessionUpdate": kind, "messageId": "m1", "content": content});
463 if kind == "session_message" {
464 base["senderSessionId"] = json!("parent");
465 base["recipientSessionId"] = json!("child");
466 }
467 {
468 let key = "messageId";
469 let mut missing = base.clone();
470 missing.as_object_mut().unwrap().remove(key);
471 assert!(
472 serde_json::from_value::<SessionUpdate>(missing).is_err(),
473 "{kind} {key}"
474 );
475 let mut null = base.clone();
476 null[key] = Value::Null;
477 assert!(
478 serde_json::from_value::<SessionUpdate>(null).is_err(),
479 "{kind} {key}"
480 );
481 let mut non_string = base.clone();
482 non_string[key] = json!(42);
483 assert!(
484 serde_json::from_value::<SessionUpdate>(non_string).is_err(),
485 "{kind} {key}"
486 );
487 }
488 if kind == "session_message_chunk" {
489 for content in [None, Some(Value::Null), Some(json!([]))] {
490 let mut invalid = base.clone();
491 invalid.as_object_mut().unwrap().remove("content");
492 if let Some(content) = content {
493 invalid["content"] = content;
494 }
495 assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
496 }
497 } else {
498 for content in [None, Some(Value::Null), Some(json!([])), Some(json!({}))] {
499 let mut wire = base.clone();
500 wire.as_object_mut().unwrap().remove("content");
501 if let Some(content) = content {
502 wire["content"] = content;
503 }
504 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
505 let encoded = serde_json::to_value(decoded).unwrap();
506 if wire["content"].is_object() {
507 wire.as_object_mut().unwrap().remove("content");
508 }
509 assert_eq!(encoded, wire);
510 }
511 }
512 for meta in [None, Some(Value::Null), Some(json!({"tag": "value"}))] {
513 let mut wire = base.clone();
514 if let Some(meta) = meta {
515 wire["_meta"] = meta;
516 }
517 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
518 let encoded = serde_json::to_value(decoded).unwrap();
519 if kind == "session_message" {
520 assert_eq!(encoded, wire);
521 } else if wire["_meta"].is_null() {
522 assert_eq!(encoded, base);
523 } else {
524 assert_eq!(encoded, wire);
525 }
526 }
527 }
528 let metadata_only = json!({"sessionUpdate": "session_message",
529 "messageId": "m1", "senderSessionId": "parent",
530 "recipientSessionId": "child", "_meta": {"tag": "value"}});
531 let decoded: SessionUpdate = serde_json::from_value(metadata_only.clone()).unwrap();
532 assert_eq!(serde_json::to_value(decoded).unwrap(), metadata_only);
533 let cleared = SessionMessage::new("m1")
534 .sender_session_id(SessionId::new("parent"))
535 .recipient_session_id(SessionId::new("child"))
536 .content(None)
537 .meta(None);
538 assert_eq!(
539 serde_json::to_value(cleared).unwrap(),
540 json!({"messageId": "m1", "senderSessionId": "parent",
541 "recipientSessionId": "child", "content": null, "_meta": null})
542 );
543 }
544
545 #[cfg(feature = "schemars")]
546 #[test]
547 fn schemas_require_ids_and_chunk_content_but_not_upsert_content() {
548 let upsert = serde_json::to_value(schemars::schema_for!(SessionMessage)).unwrap();
549 let required = upsert["required"].as_array().unwrap();
550 assert_eq!(required, &vec![json!("messageId")]);
551 let chunk = serde_json::to_value(schemars::schema_for!(SessionMessageChunk)).unwrap();
552 let required = chunk["required"].as_array().unwrap();
553 assert_eq!(required.len(), 2);
554 assert!(required.contains(&json!("messageId")));
555 assert!(required.contains(&json!("content")));
556 }
557
558 #[test]
559 fn endpoints_can_arrive_late_or_be_omitted_from_later_events() {
560 let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
561 let minimal = SessionMessage::new("m1");
562 let first = SessionMessageChunk::new("m1", block.clone());
563 assert_eq!(
564 serde_json::to_value(&minimal).unwrap(),
565 json!({"messageId": "m1"})
566 );
567 assert_eq!(
568 serde_json::to_value(&first).unwrap(),
569 json!({"messageId": "m1", "content": {"type": "text", "text": "hello"}})
570 );
571 let enriched = SessionMessage::new("m1")
572 .sender_session_id(SessionId::new("parent"))
573 .recipient_session_id(SessionId::new("child"));
574 assert_eq!(
575 enriched.sender_session_id,
576 MaybeUndefined::Value(SessionId::new("parent"))
577 );
578 assert_eq!(
579 enriched.recipient_session_id,
580 MaybeUndefined::Value(SessionId::new("child"))
581 );
582 let later = SessionMessage::new("m1").sender_session_id(SessionId::new("parent"));
583 assert_eq!(
584 later.sender_session_id,
585 MaybeUndefined::Value(SessionId::new("parent"))
586 );
587 assert_eq!(later.recipient_session_id, MaybeUndefined::Undefined);
588 let more = SessionMessageChunk::new("m1", block);
589 for update in [
590 SessionUpdate::SessionMessage(minimal),
591 SessionUpdate::SessionMessageChunk(first),
592 SessionUpdate::SessionMessage(enriched),
593 SessionUpdate::SessionMessage(later),
594 SessionUpdate::SessionMessageChunk(more),
595 ] {
596 let wire = serde_json::to_value(&update).unwrap();
597 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
598 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
599 }
600 }
601
602 #[test]
603 fn null_endpoints_clear_invalid_are_absent_but_message_id_is_required() {
604 let mut base = json!({"sessionUpdate": "session_message", "messageId": "m1",
605 "senderSessionId": null, "recipientSessionId": 42});
606 let decoded: SessionUpdate = serde_json::from_value(base.clone()).unwrap();
607 let encoded = serde_json::to_value(decoded).unwrap();
608 base.as_object_mut().unwrap().remove("recipientSessionId");
609 assert_eq!(encoded, base);
610 for bad in [Value::Null, json!(42)] {
611 let mut invalid = base.clone();
612 invalid["messageId"] = bad;
613 assert!(serde_json::from_value::<SessionUpdate>(invalid).is_err());
614 }
615 }
616
617 #[test]
618 fn message_references_link_views_and_can_arrive_late() {
619 let block = ContentBlock::Text(crate::v2::TextContent::new("hello"));
620 let incoming = SessionMessage::new("received-1")
621 .sender_session_id(SessionId::new("parent"))
622 .recipient_session_id(SessionId::new("child"))
623 .sender_message_id(MessageId::new("sent-1"))
624 .content(vec![block]);
625 let outgoing =
627 SessionMessage::new("sent-1").recipient_message_id(MessageId::new("received-1"));
628 for (update, wire) in [
629 (
630 SessionUpdate::SessionMessage(incoming),
631 json!({"sessionUpdate": "session_message", "messageId": "received-1",
632 "senderSessionId": "parent", "recipientSessionId": "child",
633 "senderMessageId": "sent-1", "content": [{"type": "text", "text": "hello"}]}),
634 ),
635 (
636 SessionUpdate::SessionMessage(outgoing),
637 json!({"sessionUpdate": "session_message", "messageId": "sent-1",
638 "recipientMessageId": "received-1"}),
639 ),
640 ] {
641 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
642 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
643 assert_eq!(decoded, update);
644 }
645
646 let mut wire = json!({"sessionUpdate": "session_message", "messageId": "m1",
647 "senderMessageId": null, "recipientMessageId": 42});
648 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
649 wire.as_object_mut().unwrap().remove("recipientMessageId");
650 assert_eq!(
651 serde_json::to_value(decoded).unwrap(),
652 wire,
653 "null clears a reference and a malformed one is not supplied"
654 );
655 }
656}
657
658#[cfg(all(test, not(feature = "unstable_subagents")))]
659mod disabled_session_message_tests {
660 use super::*;
661 use serde_json::json;
662
663 #[test]
664 fn preserves_both_unknown_message_discriminators() {
665 for (kind, content) in [
666 ("session_message", json!([])),
667 (
668 "session_message_chunk",
669 json!({"type": "text", "text": "hello"}),
670 ),
671 ] {
672 let wire = json!({"sessionUpdate": kind, "messageId": "m1",
673 "senderSessionId": "parent", "recipientSessionId": "child", "content": content});
674 let decoded: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
675 assert!(matches!(decoded, SessionUpdate::Other(_)));
676 assert_eq!(serde_json::to_value(decoded).unwrap(), wire);
677 }
678 }
679}
680
681#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
684#[serde(rename_all = "snake_case")]
685#[non_exhaustive]
686pub enum NoticeSeverity {
687 Info,
689 Warning,
691 Error,
693 #[serde(untagged)]
698 Other(String),
699}
700
701#[serde_as]
709#[skip_serializing_none]
710#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
711#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
712#[serde(rename_all = "camelCase")]
713#[non_exhaustive]
714pub struct Notice {
715 pub severity: NoticeSeverity,
717 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
719 pub title: String,
720 #[serde_as(deserialize_as = "DefaultOnError")]
724 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
725 #[serde(default)]
726 pub description: Option<String>,
727 #[serde_as(deserialize_as = "DefaultOnError")]
733 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
734 #[serde(default, rename = "_meta")]
735 pub meta: Option<Meta>,
736}
737
738impl Notice {
739 #[must_use]
741 pub fn new(severity: NoticeSeverity, title: impl Into<String>) -> Self {
742 Self {
743 severity,
744 title: title.into(),
745 description: None,
746 meta: None,
747 }
748 }
749
750 #[must_use]
752 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
753 self.description = description.into_option();
754 self
755 }
756
757 #[must_use]
759 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
760 self.meta = meta.into_option();
761 self
762 }
763}
764
765#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
767#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
768#[serde(transparent)]
769#[from(forward)]
770#[non_exhaustive]
771pub struct CompactionId(pub Arc<str>);
772
773impl CompactionId {
774 #[must_use]
776 pub fn new(id: impl Into<Self>) -> Self {
777 id.into()
778 }
779}
780
781#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
783#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
784#[serde(rename_all = "snake_case")]
785#[non_exhaustive]
786pub enum CompactionStatus {
787 #[default]
789 InProgress,
790 Completed,
792 Failed,
794 Cancelled,
796 #[serde(untagged)]
801 Other(String),
802}
803
804#[serde_as]
812#[skip_serializing_none]
813#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
814#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
815#[serde(rename_all = "camelCase")]
816#[non_exhaustive]
817pub struct CompactionUpdate {
818 pub compaction_id: CompactionId,
820 pub status: CompactionStatus,
822 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
824 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
825 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
826 pub summary: MaybeUndefined<Vec<ContentBlock>>,
827 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
829 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
830 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
831 pub error: MaybeUndefined<String>,
832 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
841 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
842 #[serde(
843 rename = "_meta",
844 default,
845 skip_serializing_if = "MaybeUndefined::is_undefined"
846 )]
847 pub meta: MaybeUndefined<Meta>,
848}
849
850impl CompactionUpdate {
851 #[must_use]
853 pub fn new(compaction_id: impl Into<CompactionId>, status: CompactionStatus) -> Self {
854 Self {
855 compaction_id: compaction_id.into(),
856 status,
857 summary: MaybeUndefined::Undefined,
858 error: MaybeUndefined::Undefined,
859 meta: MaybeUndefined::Undefined,
860 }
861 }
862
863 #[must_use]
865 pub fn summary(mut self, summary: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
866 self.summary = summary.into_maybe_undefined();
867 self
868 }
869
870 #[must_use]
872 pub fn error(mut self, error: impl IntoMaybeUndefined<String>) -> Self {
873 self.error = error.into_maybe_undefined();
874 self
875 }
876
877 #[must_use]
882 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
883 self.meta = meta.into_maybe_undefined();
884 self
885 }
886}
887
888#[serde_as]
891#[skip_serializing_none]
892#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
893#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
894#[serde(rename_all = "camelCase")]
895#[non_exhaustive]
896pub struct CompactionSummaryChunk {
897 pub compaction_id: CompactionId,
899 pub content: ContentBlock,
901 #[serde_as(deserialize_as = "DefaultOnError")]
908 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
909 #[serde(default, rename = "_meta")]
910 pub meta: Option<Meta>,
911}
912
913impl CompactionSummaryChunk {
914 #[must_use]
916 pub fn new(compaction_id: impl Into<CompactionId>, content: ContentBlock) -> Self {
917 Self {
918 compaction_id: compaction_id.into(),
919 content,
920 meta: None,
921 }
922 }
923
924 #[must_use]
926 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
927 self.meta = meta.into_option();
928 self
929 }
930}
931
932#[cfg(feature = "unstable_subagents")]
969#[serde_as]
970#[skip_serializing_none]
971#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
972#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
973#[serde(rename_all = "camelCase")]
974#[non_exhaustive]
975pub struct SubagentUpdate {
976 pub session_id: SessionId,
978 #[serde_as(deserialize_as = "DefaultOnError")]
983 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
984 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
985 pub title: MaybeUndefined<String>,
986 #[serde_as(deserialize_as = "DefaultOnError")]
991 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
992 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
993 pub description: MaybeUndefined<String>,
994 #[serde_as(deserialize_as = "DefaultOnError")]
999 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1000 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1001 pub capabilities: MaybeUndefined<SubagentSessionCapabilities>,
1002 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1008 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1009 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1010 pub state: MaybeUndefined<StateUpdate>,
1011 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1017 pub cwd: MaybeUndefined<AbsolutePath>,
1018 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1025 pub additional_directories: MaybeUndefined<Vec<AbsolutePath>>,
1026 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1035 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1036 #[serde(
1037 rename = "_meta",
1038 default,
1039 skip_serializing_if = "MaybeUndefined::is_undefined"
1040 )]
1041 pub meta: MaybeUndefined<Meta>,
1042}
1043
1044#[cfg(feature = "unstable_subagents")]
1045impl SubagentUpdate {
1046 #[must_use]
1048 pub fn new(session_id: impl Into<SessionId>) -> Self {
1049 Self {
1050 session_id: session_id.into(),
1051 title: MaybeUndefined::Undefined,
1052 description: MaybeUndefined::Undefined,
1053 capabilities: MaybeUndefined::Undefined,
1054 state: MaybeUndefined::Undefined,
1055 cwd: MaybeUndefined::Undefined,
1056 additional_directories: MaybeUndefined::Undefined,
1057 meta: MaybeUndefined::Undefined,
1058 }
1059 }
1060
1061 #[must_use]
1063 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1064 self.title = title.into_maybe_undefined();
1065 self
1066 }
1067
1068 #[must_use]
1070 pub fn description(mut self, description: impl IntoMaybeUndefined<String>) -> Self {
1071 self.description = description.into_maybe_undefined();
1072 self
1073 }
1074
1075 #[must_use]
1077 pub fn capabilities(
1078 mut self,
1079 capabilities: impl IntoMaybeUndefined<SubagentSessionCapabilities>,
1080 ) -> Self {
1081 self.capabilities = capabilities.into_maybe_undefined();
1082 self
1083 }
1084
1085 #[must_use]
1087 pub fn state(mut self, state: impl IntoMaybeUndefined<StateUpdate>) -> Self {
1088 self.state = state.into_maybe_undefined();
1089 self
1090 }
1091
1092 #[must_use]
1096 pub fn cwd(mut self, cwd: impl IntoMaybeUndefined<AbsolutePath>) -> Self {
1097 self.cwd = cwd.into_maybe_undefined();
1098 self
1099 }
1100
1101 #[must_use]
1105 pub fn additional_directories(
1106 mut self,
1107 additional_directories: impl IntoMaybeUndefined<Vec<AbsolutePath>>,
1108 ) -> Self {
1109 self.additional_directories = additional_directories.into_maybe_undefined();
1110 self
1111 }
1112
1113 #[must_use]
1118 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1119 self.meta = meta.into_maybe_undefined();
1120 self
1121 }
1122}
1123
1124#[cfg(feature = "unstable_subagents")]
1133#[serde_as]
1134#[skip_serializing_none]
1135#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1136#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1137#[serde(rename_all = "camelCase")]
1138#[non_exhaustive]
1139pub struct SubagentSessionCapabilities {
1140 #[serde_as(deserialize_as = "DefaultOnError")]
1144 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1145 #[serde(default)]
1146 pub cancel: Option<SessionCancelCapabilities>,
1147 #[serde_as(deserialize_as = "DefaultOnError")]
1153 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1154 #[serde(default)]
1155 #[serde(rename = "_meta")]
1156 pub meta: Option<Meta>,
1157}
1158
1159#[cfg(feature = "unstable_subagents")]
1160impl SubagentSessionCapabilities {
1161 #[must_use]
1163 pub fn new() -> Self {
1164 Self::default()
1165 }
1166
1167 #[must_use]
1169 pub fn cancel(mut self, cancel: impl IntoOption<SessionCancelCapabilities>) -> Self {
1170 self.cancel = cancel.into_option();
1171 self
1172 }
1173
1174 #[must_use]
1178 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1179 self.meta = meta.into_option();
1180 self
1181 }
1182}
1183
1184#[cfg(feature = "unstable_subagents")]
1192#[serde_as]
1193#[skip_serializing_none]
1194#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1195#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1196#[non_exhaustive]
1197pub struct SessionCancelCapabilities {
1198 #[serde_as(deserialize_as = "DefaultOnError")]
1204 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1205 #[serde(default)]
1206 #[serde(rename = "_meta")]
1207 pub meta: Option<Meta>,
1208}
1209
1210#[cfg(feature = "unstable_subagents")]
1211impl SessionCancelCapabilities {
1212 #[must_use]
1214 pub fn new() -> Self {
1215 Self::default()
1216 }
1217
1218 #[must_use]
1224 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1225 self.meta = meta.into_option();
1226 self
1227 }
1228}
1229
1230#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1236#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1237#[cfg_attr(feature = "schemars", schemars(inline))]
1238#[cfg_attr(feature = "schemars", schemars(transform = other_session_update_schema))]
1239#[serde(rename_all = "camelCase")]
1240#[non_exhaustive]
1241pub struct OtherSessionUpdate {
1242 #[serde(rename = "sessionUpdate")]
1248 pub session_update: String,
1249 #[serde(flatten)]
1251 pub fields: BTreeMap<String, serde_json::Value>,
1252}
1253
1254impl OtherSessionUpdate {
1255 #[must_use]
1257 pub fn new(
1258 session_update: impl Into<String>,
1259 mut fields: BTreeMap<String, serde_json::Value>,
1260 ) -> Self {
1261 fields.remove("sessionUpdate");
1262 Self {
1263 session_update: session_update.into(),
1264 fields,
1265 }
1266 }
1267}
1268
1269impl<'de> Deserialize<'de> for OtherSessionUpdate {
1270 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1271 where
1272 D: serde::Deserializer<'de>,
1273 {
1274 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1275 let session_update = fields
1276 .remove("sessionUpdate")
1277 .ok_or_else(|| serde::de::Error::missing_field("sessionUpdate"))?;
1278 let serde_json::Value::String(session_update) = session_update else {
1279 return Err(serde::de::Error::custom("`sessionUpdate` must be a string"));
1280 };
1281
1282 if is_known_session_update(&session_update) {
1283 return Err(serde::de::Error::custom(format!(
1284 "known session update `{session_update}` did not match its schema"
1285 )));
1286 }
1287
1288 Ok(Self {
1289 session_update,
1290 fields,
1291 })
1292 }
1293}
1294
1295fn is_known_session_update(session_update: &str) -> bool {
1296 if session_update == "notice" {
1297 return true;
1298 }
1299 if matches!(
1300 session_update,
1301 "compaction_update" | "compaction_summary_chunk"
1302 ) {
1303 return true;
1304 }
1305 #[cfg(feature = "unstable_plan_operations")]
1306 if session_update == "plan_removed" {
1307 return true;
1308 }
1309 #[cfg(feature = "unstable_subagents")]
1310 if matches!(
1311 session_update,
1312 "subagent_update" | "session_message" | "session_message_chunk"
1313 ) {
1314 return true;
1315 }
1316 matches!(
1317 session_update,
1318 "user_message_chunk"
1319 | "user_message"
1320 | "agent_message_chunk"
1321 | "agent_message"
1322 | "agent_thought_chunk"
1323 | "agent_thought"
1324 | "state_update"
1325 | "tool_call_content_chunk"
1326 | "tool_call_update"
1327 | "terminal_update"
1328 | "terminal_output_chunk"
1329 | "plan_update"
1330 | "available_commands_update"
1331 | "config_option_update"
1332 | "session_info_update"
1333 | "usage_update"
1334 )
1335}
1336
1337#[cfg(feature = "schemars")]
1338fn other_session_update_schema(schema: &mut Schema) {
1339 super::schema_util::reject_known_string_discriminators(
1340 schema,
1341 "sessionUpdate",
1342 &[
1343 "user_message_chunk",
1344 "user_message",
1345 "agent_message_chunk",
1346 "agent_message",
1347 "agent_thought_chunk",
1348 "agent_thought",
1349 "state_update",
1350 "tool_call_content_chunk",
1351 "tool_call_update",
1352 "terminal_update",
1353 "terminal_output_chunk",
1354 "plan_update",
1355 "available_commands_update",
1356 "config_option_update",
1357 "session_info_update",
1358 #[cfg(feature = "unstable_plan_operations")]
1359 "plan_removed",
1360 "usage_update",
1361 "notice",
1362 "compaction_update",
1363 "compaction_summary_chunk",
1364 #[cfg(feature = "unstable_subagents")]
1365 "subagent_update",
1366 #[cfg(feature = "unstable_subagents")]
1367 "session_message",
1368 #[cfg(feature = "unstable_subagents")]
1369 "session_message_chunk",
1370 ],
1371 );
1372}
1373
1374#[serde_as]
1376#[skip_serializing_none]
1377#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1379#[serde(rename_all = "camelCase")]
1380#[non_exhaustive]
1381pub struct ConfigOptionUpdate {
1382 #[serde_as(deserialize_as = "VecSkipError<_>")]
1384 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
1385 pub config_options: Vec<SessionConfigOption>,
1386 #[serde_as(deserialize_as = "DefaultOnError")]
1392 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1393 #[serde(default)]
1394 #[serde(rename = "_meta")]
1395 pub meta: Option<Meta>,
1396}
1397
1398impl ConfigOptionUpdate {
1399 #[must_use]
1401 pub fn new(config_options: Vec<SessionConfigOption>) -> Self {
1402 Self {
1403 config_options,
1404 meta: None,
1405 }
1406 }
1407
1408 #[must_use]
1414 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1415 self.meta = meta.into_option();
1416 self
1417 }
1418}
1419
1420#[serde_as]
1429#[skip_serializing_none]
1430#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1431#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1432#[serde(rename_all = "camelCase")]
1433#[non_exhaustive]
1434pub struct SessionInfoUpdate {
1435 #[serde_as(deserialize_as = "DefaultOnError")]
1437 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1438 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1439 pub title: MaybeUndefined<String>,
1440 #[serde_as(deserialize_as = "DefaultOnError")]
1442 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
1443 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1444 pub updated_at: MaybeUndefined<String>,
1445 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1454 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1455 #[serde(
1456 rename = "_meta",
1457 default,
1458 skip_serializing_if = "MaybeUndefined::is_undefined"
1459 )]
1460 pub meta: MaybeUndefined<Meta>,
1461}
1462
1463impl SessionInfoUpdate {
1464 #[must_use]
1466 pub fn new() -> Self {
1467 Self::default()
1468 }
1469
1470 #[must_use]
1472 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
1473 self.title = title.into_maybe_undefined();
1474 self
1475 }
1476
1477 #[must_use]
1479 pub fn updated_at(mut self, updated_at: impl IntoMaybeUndefined<String>) -> Self {
1480 self.updated_at = updated_at.into_maybe_undefined();
1481 self
1482 }
1483
1484 #[must_use]
1489 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
1490 self.meta = meta.into_maybe_undefined();
1491 self
1492 }
1493}
1494
1495#[serde_as]
1497#[skip_serializing_none]
1498#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1500#[serde(rename_all = "camelCase")]
1501#[non_exhaustive]
1502pub struct UsageUpdate {
1503 pub used: u64,
1505 pub size: u64,
1507 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
1511 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1512 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1513 pub cost: MaybeUndefined<Cost>,
1514 #[serde_as(deserialize_as = "DefaultOnError")]
1520 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1521 #[serde(default)]
1522 #[serde(rename = "_meta")]
1523 pub meta: Option<Meta>,
1524}
1525
1526impl UsageUpdate {
1527 #[must_use]
1529 pub fn new(used: u64, size: u64) -> Self {
1530 Self {
1531 used,
1532 size,
1533 cost: MaybeUndefined::Undefined,
1534 meta: None,
1535 }
1536 }
1537
1538 #[must_use]
1540 pub fn cost(mut self, cost: impl IntoMaybeUndefined<Cost>) -> Self {
1541 self.cost = cost.into_maybe_undefined();
1542 self
1543 }
1544
1545 #[must_use]
1551 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1552 self.meta = meta.into_option();
1553 self
1554 }
1555}
1556
1557#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1598#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1599#[serde(tag = "state", rename_all = "snake_case")]
1600#[non_exhaustive]
1601pub enum StateUpdate {
1602 Running(RunningStateUpdate),
1604 Idle(IdleStateUpdate),
1606 RequiresAction(RequiresActionStateUpdate),
1608 #[cfg(feature = "unstable_subagents")]
1615 Unknown(UnknownStateUpdate),
1616 #[serde(untagged)]
1628 Other(OtherStateUpdate),
1629}
1630
1631impl StateUpdate {
1632 #[must_use]
1640 pub fn background(&self) -> Option<&[BackgroundWork]> {
1641 match self {
1642 Self::Running(state) => state.background.as_deref(),
1643 Self::Idle(state) => state.background.as_deref(),
1644 Self::RequiresAction(state) => state.background.as_deref(),
1645 #[cfg(feature = "unstable_subagents")]
1646 Self::Unknown(state) => state.background.as_deref(),
1647 Self::Other(_) => None,
1648 }
1649 }
1650}
1651
1652#[serde_as]
1654#[skip_serializing_none]
1655#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1656#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1657#[serde(rename_all = "camelCase")]
1658#[non_exhaustive]
1659pub struct RunningStateUpdate {
1660 #[serde_as(deserialize_as = "DefaultOnError")]
1669 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1670 #[serde(default)]
1671 pub background: Option<Vec<BackgroundWork>>,
1672 #[serde_as(deserialize_as = "DefaultOnError")]
1678 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1679 #[serde(default)]
1680 #[serde(rename = "_meta")]
1681 pub meta: Option<Meta>,
1682}
1683
1684impl RunningStateUpdate {
1685 #[must_use]
1687 pub fn new() -> Self {
1688 Self::default()
1689 }
1690
1691 #[must_use]
1693 pub fn background(mut self, background: impl IntoOption<Vec<BackgroundWork>>) -> Self {
1694 self.background = background.into_option();
1695 self
1696 }
1697
1698 #[must_use]
1704 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1705 self.meta = meta.into_option();
1706 self
1707 }
1708}
1709
1710#[serde_as]
1719#[skip_serializing_none]
1720#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1721#[cfg_attr(feature = "schemars", schemars(transform = idle_state_update_schema))]
1722#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1723#[serde(rename_all = "camelCase")]
1724#[non_exhaustive]
1725pub struct IdleStateUpdate {
1726 #[serde(flatten)]
1728 pub stop_reason: Option<StopReason>,
1729 #[cfg(feature = "unstable_end_turn_token_usage")]
1738 #[serde_as(deserialize_as = "DefaultOnError")]
1739 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1740 #[serde(default)]
1741 pub usage: Option<Box<Usage>>,
1742 #[serde_as(deserialize_as = "DefaultOnError")]
1751 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1752 #[serde(default)]
1753 pub background: Option<Vec<BackgroundWork>>,
1754 #[serde_as(deserialize_as = "DefaultOnError")]
1760 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1761 #[serde(default)]
1762 #[serde(rename = "_meta")]
1763 pub meta: Option<Meta>,
1764}
1765
1766impl IdleStateUpdate {
1767 #[must_use]
1769 pub fn new() -> Self {
1770 Self::default()
1771 }
1772
1773 #[must_use]
1775 pub fn stop_reason(mut self, stop_reason: impl IntoOption<StopReason>) -> Self {
1776 self.stop_reason = stop_reason.into_option();
1777 self
1778 }
1779
1780 #[cfg(feature = "unstable_end_turn_token_usage")]
1786 #[must_use]
1787 pub fn usage(mut self, usage: impl IntoOption<Usage>) -> Self {
1788 self.usage = usage.into_option().map(Box::new);
1789 self
1790 }
1791
1792 #[must_use]
1794 pub fn background(mut self, background: impl IntoOption<Vec<BackgroundWork>>) -> Self {
1795 self.background = background.into_option();
1796 self
1797 }
1798
1799 #[must_use]
1805 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1806 self.meta = meta.into_option();
1807 self
1808 }
1809}
1810
1811#[cfg(feature = "schemars")]
1820fn idle_state_update_schema(schema: &mut Schema) {
1821 if let Some(properties) = schema
1822 .get_mut("properties")
1823 .and_then(serde_json::Value::as_object_mut)
1824 {
1825 properties.insert(
1826 "stopReason".into(),
1827 serde_json::json!({
1828 "description": "Why foreground work stopped. The value selects one of this type's variants, which may add fields of their own.\n\nOptional. Omitted and `null` are equivalent and mean the agent is not reporting a stop reason; a malformed value is treated the same way.\n\nSee protocol docs: [Stop Reasons](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#stop-reasons)",
1829 "type": ["string", "null"],
1830 "x-deserialize-default-on-error": true
1831 }),
1832 );
1833 }
1834 let Some(variants) = schema
1835 .get_mut("anyOf")
1836 .and_then(serde_json::Value::as_array_mut)
1837 else {
1838 return;
1839 };
1840 for variant in variants {
1841 if variant.as_object().is_some_and(serde_json::Map::is_empty) {
1842 *variant = serde_json::json!({
1843 "title": "none",
1844 "description": "No stop reason: `stopReason` is omitted or `null`.",
1845 "type": "object",
1846 "properties": {
1847 "stopReason": { "type": "null" }
1848 }
1849 });
1850 }
1851 }
1852}
1853
1854#[serde_as]
1863#[skip_serializing_none]
1864#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1865#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1866#[serde(rename_all = "camelCase")]
1867#[non_exhaustive]
1868pub struct RequiresActionStateUpdate {
1869 #[serde_as(deserialize_as = "DefaultOnError")]
1878 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1879 #[serde(default)]
1880 pub background: Option<Vec<BackgroundWork>>,
1881 #[serde_as(deserialize_as = "DefaultOnError")]
1887 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1888 #[serde(default)]
1889 #[serde(rename = "_meta")]
1890 pub meta: Option<Meta>,
1891}
1892
1893impl RequiresActionStateUpdate {
1894 #[must_use]
1896 pub fn new() -> Self {
1897 Self::default()
1898 }
1899
1900 #[must_use]
1902 pub fn background(mut self, background: impl IntoOption<Vec<BackgroundWork>>) -> Self {
1903 self.background = background.into_option();
1904 self
1905 }
1906
1907 #[must_use]
1913 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1914 self.meta = meta.into_option();
1915 self
1916 }
1917}
1918
1919#[cfg(feature = "unstable_subagents")]
1933#[serde_as]
1934#[skip_serializing_none]
1935#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1936#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1937#[serde(rename_all = "camelCase")]
1938#[non_exhaustive]
1939pub struct UnknownStateUpdate {
1940 #[serde_as(deserialize_as = "DefaultOnError")]
1949 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1950 #[serde(default)]
1951 pub background: Option<Vec<BackgroundWork>>,
1952 #[serde_as(deserialize_as = "DefaultOnError")]
1958 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1959 #[serde(default, rename = "_meta")]
1960 pub meta: Option<Meta>,
1961}
1962
1963#[cfg(feature = "unstable_subagents")]
1964impl UnknownStateUpdate {
1965 #[must_use]
1967 pub fn new() -> Self {
1968 Self::default()
1969 }
1970
1971 #[must_use]
1973 pub fn background(mut self, background: impl IntoOption<Vec<BackgroundWork>>) -> Self {
1974 self.background = background.into_option();
1975 self
1976 }
1977
1978 #[must_use]
1980 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1981 self.meta = meta.into_option();
1982 self
1983 }
1984}
1985
1986#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1991#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1992#[cfg_attr(feature = "schemars", schemars(inline))]
1993#[cfg_attr(feature = "schemars", schemars(transform = other_state_update_schema))]
1994#[serde(rename_all = "camelCase")]
1995#[non_exhaustive]
1996pub struct OtherStateUpdate {
1997 #[serde(rename = "state")]
2003 pub state: String,
2004 #[serde(flatten)]
2006 pub fields: BTreeMap<String, serde_json::Value>,
2007}
2008
2009impl OtherStateUpdate {
2010 #[must_use]
2012 pub fn new(state: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2013 fields.remove("state");
2014 Self {
2015 state: state.into(),
2016 fields,
2017 }
2018 }
2019}
2020
2021impl<'de> Deserialize<'de> for OtherStateUpdate {
2022 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2023 where
2024 D: serde::Deserializer<'de>,
2025 {
2026 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2027 let state = fields
2028 .remove("state")
2029 .ok_or_else(|| serde::de::Error::missing_field("state"))?;
2030 let serde_json::Value::String(state) = state else {
2031 return Err(serde::de::Error::custom("`state` must be a string"));
2032 };
2033
2034 if is_known_state_update(&state) {
2035 return Err(serde::de::Error::custom(format!(
2036 "known state update `{state}` did not match its schema"
2037 )));
2038 }
2039
2040 Ok(Self { state, fields })
2041 }
2042}
2043
2044const KNOWN_STATE_UPDATE_STATES: &[&str] = &[
2045 "running",
2046 "idle",
2047 "requires_action",
2048 #[cfg(feature = "unstable_subagents")]
2049 "unknown",
2050];
2051
2052fn is_known_state_update(state: &str) -> bool {
2053 KNOWN_STATE_UPDATE_STATES.contains(&state)
2054}
2055
2056#[cfg(feature = "schemars")]
2057fn other_state_update_schema(schema: &mut Schema) {
2058 super::schema_util::reject_known_string_discriminators(
2059 schema,
2060 "state",
2061 KNOWN_STATE_UPDATE_STATES,
2062 );
2063}
2064
2065#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2074#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2075#[serde(tag = "type", rename_all = "snake_case")]
2076#[non_exhaustive]
2077pub enum BackgroundWork {
2078 Terminal(BackgroundTerminal),
2081 ToolCall(BackgroundToolCall),
2085 #[cfg(feature = "unstable_subagents")]
2092 Subagent(BackgroundSubagent),
2093 #[serde(untagged)]
2102 Other(OtherBackgroundWork),
2103}
2104
2105#[serde_as]
2107#[skip_serializing_none]
2108#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2110#[serde(rename_all = "camelCase")]
2111#[non_exhaustive]
2112pub struct BackgroundTerminal {
2113 pub terminal_id: TerminalId,
2115 #[serde_as(deserialize_as = "DefaultOnError")]
2121 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2122 #[serde(default)]
2123 #[serde(rename = "_meta")]
2124 pub meta: Option<Meta>,
2125}
2126
2127impl BackgroundTerminal {
2128 #[must_use]
2130 pub fn new(terminal_id: impl Into<TerminalId>) -> Self {
2131 Self {
2132 terminal_id: terminal_id.into(),
2133 meta: None,
2134 }
2135 }
2136
2137 #[must_use]
2143 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2144 self.meta = meta.into_option();
2145 self
2146 }
2147}
2148
2149#[serde_as]
2151#[skip_serializing_none]
2152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2154#[serde(rename_all = "camelCase")]
2155#[non_exhaustive]
2156pub struct BackgroundToolCall {
2157 pub tool_call_id: ToolCallId,
2159 #[serde_as(deserialize_as = "DefaultOnError")]
2165 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2166 #[serde(default)]
2167 #[serde(rename = "_meta")]
2168 pub meta: Option<Meta>,
2169}
2170
2171impl BackgroundToolCall {
2172 #[must_use]
2174 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
2175 Self {
2176 tool_call_id: tool_call_id.into(),
2177 meta: None,
2178 }
2179 }
2180
2181 #[must_use]
2187 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2188 self.meta = meta.into_option();
2189 self
2190 }
2191}
2192
2193#[cfg(feature = "unstable_subagents")]
2199#[serde_as]
2200#[skip_serializing_none]
2201#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2203#[serde(rename_all = "camelCase")]
2204#[non_exhaustive]
2205pub struct BackgroundSubagent {
2206 pub session_id: SessionId,
2208 #[serde_as(deserialize_as = "DefaultOnError")]
2214 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2215 #[serde(default)]
2216 #[serde(rename = "_meta")]
2217 pub meta: Option<Meta>,
2218}
2219
2220#[cfg(feature = "unstable_subagents")]
2221impl BackgroundSubagent {
2222 #[must_use]
2224 pub fn new(session_id: impl Into<SessionId>) -> Self {
2225 Self {
2226 session_id: session_id.into(),
2227 meta: None,
2228 }
2229 }
2230
2231 #[must_use]
2237 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2238 self.meta = meta.into_option();
2239 self
2240 }
2241}
2242
2243#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2248#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2249#[cfg_attr(feature = "schemars", schemars(inline))]
2250#[cfg_attr(feature = "schemars", schemars(transform = other_background_work_schema))]
2251#[serde(rename_all = "camelCase")]
2252#[non_exhaustive]
2253pub struct OtherBackgroundWork {
2254 #[serde(rename = "type")]
2260 pub kind: String,
2261 #[serde(flatten)]
2263 pub fields: BTreeMap<String, serde_json::Value>,
2264}
2265
2266impl OtherBackgroundWork {
2267 #[must_use]
2269 pub fn new(kind: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2270 fields.remove("type");
2271 Self {
2272 kind: kind.into(),
2273 fields,
2274 }
2275 }
2276}
2277
2278impl<'de> Deserialize<'de> for OtherBackgroundWork {
2279 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2280 where
2281 D: serde::Deserializer<'de>,
2282 {
2283 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2284 let kind = fields
2285 .remove("type")
2286 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2287 let serde_json::Value::String(kind) = kind else {
2288 return Err(serde::de::Error::custom("`type` must be a string"));
2289 };
2290
2291 if KNOWN_BACKGROUND_WORK_TYPES.contains(&kind.as_str()) {
2292 return Err(serde::de::Error::custom(format!(
2293 "known background work `{kind}` did not match its schema"
2294 )));
2295 }
2296
2297 Ok(Self { kind, fields })
2298 }
2299}
2300
2301const KNOWN_BACKGROUND_WORK_TYPES: &[&str] = &[
2302 "terminal",
2303 "tool_call",
2304 #[cfg(feature = "unstable_subagents")]
2305 "subagent",
2306];
2307
2308#[cfg(feature = "schemars")]
2309fn other_background_work_schema(schema: &mut Schema) {
2310 super::schema_util::reject_known_string_discriminators(
2311 schema,
2312 "type",
2313 KNOWN_BACKGROUND_WORK_TYPES,
2314 );
2315}
2316
2317#[serde_as]
2319#[skip_serializing_none]
2320#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2321#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2322#[serde(rename_all = "camelCase")]
2323#[non_exhaustive]
2324pub struct Cost {
2325 pub amount: f64,
2327 pub currency: String,
2330 #[serde_as(deserialize_as = "DefaultOnError")]
2336 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2337 #[serde(default)]
2338 #[serde(rename = "_meta")]
2339 pub meta: Option<Meta>,
2340}
2341
2342impl Cost {
2343 #[must_use]
2345 pub fn new(amount: f64, currency: impl Into<String>) -> Self {
2346 Self {
2347 amount,
2348 currency: currency.into(),
2349 meta: None,
2350 }
2351 }
2352
2353 #[must_use]
2359 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2360 self.meta = meta.into_option();
2361 self
2362 }
2363}
2364
2365#[serde_as]
2367#[skip_serializing_none]
2368#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2370#[serde(rename_all = "camelCase")]
2371#[non_exhaustive]
2372pub struct ContentChunk {
2373 pub message_id: MessageId,
2378 pub content: ContentBlock,
2380 #[serde_as(deserialize_as = "DefaultOnError")]
2387 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2388 #[serde(default)]
2389 #[serde(rename = "_meta")]
2390 pub meta: Option<Meta>,
2391}
2392
2393impl ContentChunk {
2394 #[must_use]
2396 pub fn new(content: ContentBlock, message_id: impl Into<MessageId>) -> Self {
2397 Self {
2398 content,
2399 message_id: message_id.into(),
2400 meta: None,
2401 }
2402 }
2403
2404 #[must_use]
2410 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2411 self.meta = meta.into_option();
2412 self
2413 }
2414}
2415
2416#[serde_as]
2430#[skip_serializing_none]
2431#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2433#[serde(rename_all = "camelCase")]
2434#[non_exhaustive]
2435pub struct UserMessage {
2436 pub message_id: MessageId,
2438 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2440 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2441 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2442 pub content: MaybeUndefined<Vec<ContentBlock>>,
2443 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2452 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2453 #[serde(
2454 rename = "_meta",
2455 default,
2456 skip_serializing_if = "MaybeUndefined::is_undefined"
2457 )]
2458 pub meta: MaybeUndefined<Meta>,
2459}
2460
2461impl UserMessage {
2462 #[must_use]
2464 pub fn new(message_id: impl Into<MessageId>) -> Self {
2465 Self {
2466 message_id: message_id.into(),
2467 content: MaybeUndefined::Undefined,
2468 meta: MaybeUndefined::Undefined,
2469 }
2470 }
2471
2472 #[must_use]
2474 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2475 self.content = content.into_maybe_undefined();
2476 self
2477 }
2478
2479 #[must_use]
2484 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2485 self.meta = meta.into_maybe_undefined();
2486 self
2487 }
2488}
2489
2490#[serde_as]
2504#[skip_serializing_none]
2505#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2506#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2507#[serde(rename_all = "camelCase")]
2508#[non_exhaustive]
2509pub struct AgentMessage {
2510 pub message_id: MessageId,
2512 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2514 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2515 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2516 pub content: MaybeUndefined<Vec<ContentBlock>>,
2517 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2526 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2527 #[serde(
2528 rename = "_meta",
2529 default,
2530 skip_serializing_if = "MaybeUndefined::is_undefined"
2531 )]
2532 pub meta: MaybeUndefined<Meta>,
2533}
2534
2535impl AgentMessage {
2536 #[must_use]
2538 pub fn new(message_id: impl Into<MessageId>) -> Self {
2539 Self {
2540 message_id: message_id.into(),
2541 content: MaybeUndefined::Undefined,
2542 meta: MaybeUndefined::Undefined,
2543 }
2544 }
2545
2546 #[must_use]
2548 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2549 self.content = content.into_maybe_undefined();
2550 self
2551 }
2552
2553 #[must_use]
2558 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2559 self.meta = meta.into_maybe_undefined();
2560 self
2561 }
2562}
2563
2564#[serde_as]
2578#[skip_serializing_none]
2579#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2581#[serde(rename_all = "camelCase")]
2582#[non_exhaustive]
2583pub struct AgentThought {
2584 pub message_id: MessageId,
2586 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
2588 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
2589 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
2590 pub content: MaybeUndefined<Vec<ContentBlock>>,
2591 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
2600 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2601 #[serde(
2602 rename = "_meta",
2603 default,
2604 skip_serializing_if = "MaybeUndefined::is_undefined"
2605 )]
2606 pub meta: MaybeUndefined<Meta>,
2607}
2608
2609impl AgentThought {
2610 #[must_use]
2612 pub fn new(message_id: impl Into<MessageId>) -> Self {
2613 Self {
2614 message_id: message_id.into(),
2615 content: MaybeUndefined::Undefined,
2616 meta: MaybeUndefined::Undefined,
2617 }
2618 }
2619
2620 #[must_use]
2622 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ContentBlock>>) -> Self {
2623 self.content = content.into_maybe_undefined();
2624 self
2625 }
2626
2627 #[must_use]
2632 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
2633 self.meta = meta.into_maybe_undefined();
2634 self
2635 }
2636}
2637
2638#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2644#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
2645#[serde(transparent)]
2646#[from(forward)]
2647#[non_exhaustive]
2648pub struct MessageId(pub Arc<str>);
2649
2650impl MessageId {
2651 #[must_use]
2653 pub fn new(id: impl Into<Self>) -> Self {
2654 id.into()
2655 }
2656}
2657
2658#[serde_as]
2660#[skip_serializing_none]
2661#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2662#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2663#[serde(rename_all = "camelCase")]
2664#[non_exhaustive]
2665pub struct AvailableCommandsUpdate {
2666 #[serde_as(deserialize_as = "VecSkipError<_>")]
2668 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
2669 pub available_commands: Vec<AvailableCommand>,
2670 #[serde_as(deserialize_as = "DefaultOnError")]
2676 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2677 #[serde(default)]
2678 #[serde(rename = "_meta")]
2679 pub meta: Option<Meta>,
2680}
2681
2682impl AvailableCommandsUpdate {
2683 #[must_use]
2685 pub fn new(available_commands: Vec<AvailableCommand>) -> Self {
2686 Self {
2687 available_commands,
2688 meta: None,
2689 }
2690 }
2691
2692 #[must_use]
2698 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2699 self.meta = meta.into_option();
2700 self
2701 }
2702}
2703
2704#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2709#[serde(tag = "type", rename_all = "snake_case")]
2710#[non_exhaustive]
2711pub enum AvailableCommand {
2712 Command(Command),
2714 #[serde(untagged)]
2725 Other(OtherAvailableCommand),
2726}
2727
2728impl AvailableCommand {
2729 #[must_use]
2731 pub fn name(&self) -> &str {
2732 match self {
2733 Self::Command(c) => &c.name,
2734 Self::Other(c) => &c.name,
2735 }
2736 }
2737
2738 #[must_use]
2740 pub fn description(&self) -> &str {
2741 match self {
2742 Self::Command(c) => &c.description,
2743 Self::Other(c) => &c.description,
2744 }
2745 }
2746
2747 #[must_use]
2753 pub fn meta(&self) -> Option<&Meta> {
2754 match self {
2755 Self::Command(c) => c.meta.as_ref(),
2756 Self::Other(c) => c.fields.get("_meta").and_then(serde_json::Value::as_object),
2757 }
2758 }
2759}
2760
2761#[serde_as]
2763#[skip_serializing_none]
2764#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2765#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2766#[cfg_attr(feature = "schemars", schemars(inline))]
2767#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_schema))]
2768#[serde(rename_all = "camelCase")]
2769#[non_exhaustive]
2770pub struct OtherAvailableCommand {
2771 #[serde(rename = "type")]
2777 pub type_: String,
2778 pub name: String,
2781 pub description: String,
2783 #[serde(flatten)]
2786 pub fields: BTreeMap<String, serde_json::Value>,
2787}
2788
2789impl OtherAvailableCommand {
2790 #[must_use]
2792 pub fn new(
2793 type_: impl Into<String>,
2794 name: impl Into<String>,
2795 description: impl Into<String>,
2796 mut fields: BTreeMap<String, serde_json::Value>,
2797 ) -> Self {
2798 fields.remove("type");
2799 fields.remove("name");
2800 fields.remove("description");
2801 Self {
2802 type_: type_.into(),
2803 name: name.into(),
2804 description: description.into(),
2805 fields,
2806 }
2807 }
2808}
2809
2810impl<'de> Deserialize<'de> for OtherAvailableCommand {
2811 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2812 where
2813 D: serde::Deserializer<'de>,
2814 {
2815 #[derive(Deserialize)]
2816 struct RawOtherAvailableCommand {
2817 #[serde(rename = "type")]
2818 type_: String,
2819 name: String,
2820 description: String,
2821 #[serde(flatten)]
2822 fields: BTreeMap<String, serde_json::Value>,
2823 }
2824
2825 let raw = RawOtherAvailableCommand::deserialize(deserializer)?;
2826 if is_known_available_command_type(&raw.type_) {
2827 return Err(serde::de::Error::custom(format!(
2828 "known available command type `{}` did not match its schema",
2829 raw.type_
2830 )));
2831 }
2832
2833 Ok(Self {
2834 type_: raw.type_,
2835 name: raw.name,
2836 description: raw.description,
2837 fields: raw.fields,
2838 })
2839 }
2840}
2841
2842const KNOWN_AVAILABLE_COMMAND_TYPES: &[&str] = &["command"];
2843
2844fn is_known_available_command_type(type_: &str) -> bool {
2845 KNOWN_AVAILABLE_COMMAND_TYPES.contains(&type_)
2846}
2847
2848#[cfg(feature = "schemars")]
2849fn other_available_command_schema(schema: &mut Schema) {
2850 super::schema_util::reject_known_string_discriminators(
2851 schema,
2852 "type",
2853 KNOWN_AVAILABLE_COMMAND_TYPES,
2854 );
2855}
2856
2857#[serde_as]
2861#[skip_serializing_none]
2862#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2863#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2864#[serde(rename_all = "camelCase")]
2865#[non_exhaustive]
2866pub struct Command {
2867 pub name: String,
2871 pub description: String,
2873 #[serde_as(deserialize_as = "DefaultOnError")]
2877 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2878 #[serde(default)]
2879 pub input: Option<AvailableCommandInput>,
2880 #[serde_as(deserialize_as = "DefaultOnError")]
2886 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
2887 #[serde(default)]
2888 #[serde(rename = "_meta")]
2889 pub meta: Option<Meta>,
2890}
2891
2892impl Command {
2893 #[must_use]
2895 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
2896 Self {
2897 name: name.into(),
2898 description: description.into(),
2899 input: None,
2900 meta: None,
2901 }
2902 }
2903
2904 #[must_use]
2906 pub fn input(mut self, input: impl IntoOption<AvailableCommandInput>) -> Self {
2907 self.input = input.into_option();
2908 self
2909 }
2910
2911 #[must_use]
2917 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
2918 self.meta = meta.into_option();
2919 self
2920 }
2921}
2922
2923#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2925#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2926#[serde(tag = "type", rename_all = "snake_case")]
2927#[non_exhaustive]
2928pub enum AvailableCommandInput {
2929 #[serde(rename = "text")]
2931 Text(TextCommandInput),
2932 #[serde(untagged)]
2943 Other(OtherAvailableCommandInput),
2944}
2945
2946#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2948#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2949#[cfg_attr(feature = "schemars", schemars(inline))]
2950#[cfg_attr(feature = "schemars", schemars(transform = other_available_command_input_schema))]
2951#[serde(rename_all = "camelCase")]
2952#[non_exhaustive]
2953pub struct OtherAvailableCommandInput {
2954 #[serde(rename = "type")]
2960 pub type_: String,
2961 #[serde(flatten)]
2963 pub fields: BTreeMap<String, serde_json::Value>,
2964}
2965
2966impl OtherAvailableCommandInput {
2967 #[must_use]
2969 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
2970 fields.remove("type");
2971 Self {
2972 type_: type_.into(),
2973 fields,
2974 }
2975 }
2976}
2977
2978impl<'de> Deserialize<'de> for OtherAvailableCommandInput {
2979 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2980 where
2981 D: serde::Deserializer<'de>,
2982 {
2983 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
2984 let type_ = fields
2985 .remove("type")
2986 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
2987 let serde_json::Value::String(type_) = type_ else {
2988 return Err(serde::de::Error::custom("`type` must be a string"));
2989 };
2990
2991 if is_known_available_command_input_type(&type_) {
2992 return Err(serde::de::Error::custom(format!(
2993 "known available command input type `{type_}` did not match its schema"
2994 )));
2995 }
2996
2997 Ok(Self { type_, fields })
2998 }
2999}
3000
3001const KNOWN_AVAILABLE_COMMAND_INPUT_TYPES: &[&str] = &["text"];
3002
3003fn is_known_available_command_input_type(type_: &str) -> bool {
3004 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES.contains(&type_)
3005}
3006
3007#[cfg(feature = "schemars")]
3008fn other_available_command_input_schema(schema: &mut Schema) {
3009 super::schema_util::reject_known_string_discriminators(
3010 schema,
3011 "type",
3012 KNOWN_AVAILABLE_COMMAND_INPUT_TYPES,
3013 );
3014}
3015
3016#[serde_as]
3018#[skip_serializing_none]
3019#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3020#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3021#[serde(rename_all = "camelCase")]
3022#[non_exhaustive]
3023pub struct TextCommandInput {
3024 pub hint: String,
3026 #[serde_as(deserialize_as = "DefaultOnError")]
3032 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3033 #[serde(default)]
3034 #[serde(rename = "_meta")]
3035 pub meta: Option<Meta>,
3036}
3037
3038impl TextCommandInput {
3039 #[must_use]
3041 pub fn new(hint: impl Into<String>) -> Self {
3042 Self {
3043 hint: hint.into(),
3044 meta: None,
3045 }
3046 }
3047
3048 #[must_use]
3054 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3055 self.meta = meta.into_option();
3056 self
3057 }
3058}
3059
3060#[serde_as]
3068#[skip_serializing_none]
3069#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3070#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3071#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
3072#[serde(rename_all = "camelCase")]
3073#[non_exhaustive]
3074pub struct RequestPermissionRequest {
3075 pub session_id: SessionId,
3077 pub title: String,
3082 #[serde_as(deserialize_as = "DefaultOnError")]
3088 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3089 #[serde(default)]
3090 pub description: Option<String>,
3091 #[serde(default)]
3095 pub subject: Option<RequestPermissionSubject>,
3096 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))]
3099 pub options: Vec<PermissionOption>,
3100 #[serde_as(deserialize_as = "DefaultOnError")]
3106 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3107 #[serde(default)]
3108 #[serde(rename = "_meta")]
3109 pub meta: Option<Meta>,
3110}
3111
3112impl RequestPermissionRequest {
3113 #[must_use]
3115 pub fn new(
3116 session_id: impl Into<SessionId>,
3117 title: impl Into<String>,
3118 options: Vec<PermissionOption>,
3119 ) -> Self {
3120 Self {
3121 session_id: session_id.into(),
3122 title: title.into(),
3123 description: None,
3124 subject: None,
3125 options,
3126 meta: None,
3127 }
3128 }
3129
3130 #[must_use]
3132 pub fn description(mut self, description: impl IntoOption<String>) -> Self {
3133 self.description = description.into_option();
3134 self
3135 }
3136
3137 #[must_use]
3139 pub fn subject(mut self, subject: impl IntoOption<RequestPermissionSubject>) -> Self {
3140 self.subject = subject.into_option();
3141 self
3142 }
3143
3144 #[must_use]
3150 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3151 self.meta = meta.into_option();
3152 self
3153 }
3154}
3155
3156#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3159#[serde(tag = "type", rename_all = "snake_case")]
3160#[non_exhaustive]
3161pub enum RequestPermissionSubject {
3162 ToolCall(Box<ToolCallPermissionSubject>),
3164 Command(CommandPermissionSubject),
3166 #[serde(untagged)]
3177 Other(OtherRequestPermissionSubject),
3178}
3179
3180impl From<ToolCallPermissionSubject> for RequestPermissionSubject {
3181 fn from(subject: ToolCallPermissionSubject) -> Self {
3182 Self::ToolCall(Box::new(subject))
3183 }
3184}
3185
3186impl From<ToolCallUpdate> for RequestPermissionSubject {
3187 fn from(tool_call: ToolCallUpdate) -> Self {
3188 ToolCallPermissionSubject::new(tool_call).into()
3189 }
3190}
3191
3192impl From<CommandPermissionSubject> for RequestPermissionSubject {
3193 fn from(subject: CommandPermissionSubject) -> Self {
3194 Self::Command(subject)
3195 }
3196}
3197
3198#[serde_as]
3200#[skip_serializing_none]
3201#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3203#[serde(rename_all = "camelCase")]
3204#[non_exhaustive]
3205pub struct ToolCallPermissionSubject {
3206 pub tool_call: ToolCallUpdate,
3212 #[serde_as(deserialize_as = "DefaultOnError")]
3218 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3219 #[serde(default)]
3220 #[serde(rename = "_meta")]
3221 pub meta: Option<Meta>,
3222}
3223
3224impl ToolCallPermissionSubject {
3225 #[must_use]
3227 pub fn new(tool_call: ToolCallUpdate) -> Self {
3228 Self {
3229 tool_call,
3230 meta: None,
3231 }
3232 }
3233
3234 #[must_use]
3240 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3241 self.meta = meta.into_option();
3242 self
3243 }
3244}
3245
3246#[serde_as]
3248#[skip_serializing_none]
3249#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3251#[serde(rename_all = "camelCase")]
3252#[non_exhaustive]
3253pub struct CommandPermissionSubject {
3254 pub command: String,
3256 pub cwd: AbsolutePath,
3258 #[serde_as(deserialize_as = "DefaultOnError")]
3260 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3261 #[serde(default)]
3262 pub tool_call_id: Option<ToolCallId>,
3263 #[serde_as(deserialize_as = "DefaultOnError")]
3265 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3266 #[serde(default)]
3267 pub terminal_id: Option<TerminalId>,
3268 #[serde_as(deserialize_as = "DefaultOnError")]
3274 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3275 #[serde(default)]
3276 #[serde(rename = "_meta")]
3277 pub meta: Option<Meta>,
3278}
3279
3280impl CommandPermissionSubject {
3281 #[must_use]
3283 pub fn new(command: impl Into<String>, cwd: impl Into<AbsolutePath>) -> Self {
3284 Self {
3285 command: command.into(),
3286 cwd: cwd.into(),
3287 tool_call_id: None,
3288 terminal_id: None,
3289 meta: None,
3290 }
3291 }
3292
3293 #[must_use]
3295 pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
3296 self.tool_call_id = tool_call_id.into_option();
3297 self
3298 }
3299
3300 #[must_use]
3302 pub fn terminal_id(mut self, terminal_id: impl IntoOption<TerminalId>) -> Self {
3303 self.terminal_id = terminal_id.into_option();
3304 self
3305 }
3306
3307 #[must_use]
3309 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3310 self.meta = meta.into_option();
3311 self
3312 }
3313}
3314
3315#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3317#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3318#[cfg_attr(feature = "schemars", schemars(inline))]
3319#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_subject_schema))]
3320#[serde(rename_all = "camelCase")]
3321#[non_exhaustive]
3322pub struct OtherRequestPermissionSubject {
3323 #[serde(rename = "type")]
3329 pub type_: String,
3330 #[serde(flatten)]
3332 pub fields: BTreeMap<String, serde_json::Value>,
3333}
3334
3335impl OtherRequestPermissionSubject {
3336 #[must_use]
3338 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
3339 fields.remove("type");
3340 Self {
3341 type_: type_.into(),
3342 fields,
3343 }
3344 }
3345}
3346
3347impl<'de> Deserialize<'de> for OtherRequestPermissionSubject {
3348 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3349 where
3350 D: serde::Deserializer<'de>,
3351 {
3352 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
3353 let type_ = fields
3354 .remove("type")
3355 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
3356 let serde_json::Value::String(type_) = type_ else {
3357 return Err(serde::de::Error::custom("`type` must be a string"));
3358 };
3359
3360 if is_known_request_permission_subject_type(&type_) {
3361 return Err(serde::de::Error::custom(format!(
3362 "known request permission subject `{type_}` did not match its schema"
3363 )));
3364 }
3365
3366 Ok(Self { type_, fields })
3367 }
3368}
3369
3370fn is_known_request_permission_subject_type(type_: &str) -> bool {
3371 matches!(type_, "tool_call" | "command")
3372}
3373
3374#[cfg(feature = "schemars")]
3375fn other_request_permission_subject_schema(schema: &mut Schema) {
3376 super::schema_util::reject_known_string_discriminators(
3377 schema,
3378 "type",
3379 &["tool_call", "command"],
3380 );
3381}
3382
3383#[serde_as]
3385#[skip_serializing_none]
3386#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3387#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3388#[serde(rename_all = "camelCase")]
3389#[non_exhaustive]
3390pub struct PermissionOption {
3391 pub option_id: PermissionOptionId,
3393 pub name: String,
3395 pub kind: PermissionOptionKind,
3397 #[serde_as(deserialize_as = "DefaultOnError")]
3403 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3404 #[serde(default)]
3405 #[serde(rename = "_meta")]
3406 pub meta: Option<Meta>,
3407}
3408
3409impl PermissionOption {
3410 #[must_use]
3412 pub fn new(
3413 option_id: impl Into<PermissionOptionId>,
3414 name: impl Into<String>,
3415 kind: PermissionOptionKind,
3416 ) -> Self {
3417 Self {
3418 option_id: option_id.into(),
3419 name: name.into(),
3420 kind,
3421 meta: None,
3422 }
3423 }
3424
3425 #[must_use]
3431 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3432 self.meta = meta.into_option();
3433 self
3434 }
3435}
3436
3437#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
3440#[serde(transparent)]
3441#[from(forward)]
3442#[non_exhaustive]
3443pub struct PermissionOptionId(pub Arc<str>);
3444
3445impl PermissionOptionId {
3446 #[must_use]
3448 pub fn new(id: impl Into<Self>) -> Self {
3449 id.into()
3450 }
3451}
3452
3453#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3458#[serde(rename_all = "snake_case")]
3459#[non_exhaustive]
3460pub enum PermissionOptionKind {
3461 AllowOnce,
3463 AllowAlways,
3465 RejectOnce,
3467 RejectAlways,
3469 #[serde(untagged)]
3475 Other(String),
3476}
3477
3478#[serde_as]
3480#[skip_serializing_none]
3481#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3483#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = SESSION_REQUEST_PERMISSION_METHOD_NAME)))]
3484#[serde(rename_all = "camelCase")]
3485#[non_exhaustive]
3486pub struct RequestPermissionResponse {
3487 pub outcome: RequestPermissionOutcome,
3489 #[serde_as(deserialize_as = "DefaultOnError")]
3495 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3496 #[serde(default)]
3497 #[serde(rename = "_meta")]
3498 pub meta: Option<Meta>,
3499}
3500
3501impl RequestPermissionResponse {
3502 #[must_use]
3504 pub fn new(outcome: RequestPermissionOutcome) -> Self {
3505 Self {
3506 outcome,
3507 meta: None,
3508 }
3509 }
3510
3511 #[must_use]
3517 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3518 self.meta = meta.into_option();
3519 self
3520 }
3521}
3522
3523#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3525#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3526#[serde(tag = "outcome", rename_all = "snake_case")]
3527#[non_exhaustive]
3528pub enum RequestPermissionOutcome {
3529 Cancelled(CancelledPermissionOutcome),
3540 #[serde(rename_all = "camelCase")]
3545 Selected(SelectedPermissionOutcome),
3546 #[serde(untagged)]
3557 Other(OtherRequestPermissionOutcome),
3558}
3559
3560#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3566#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3567#[cfg_attr(feature = "schemars", schemars(inline))]
3568#[cfg_attr(feature = "schemars", schemars(transform = other_request_permission_outcome_schema))]
3569#[serde(rename_all = "camelCase")]
3570#[non_exhaustive]
3571pub struct OtherRequestPermissionOutcome {
3572 pub outcome: String,
3578 #[serde(flatten)]
3580 pub fields: BTreeMap<String, serde_json::Value>,
3581}
3582
3583impl OtherRequestPermissionOutcome {
3584 #[must_use]
3586 pub fn new(
3587 outcome: impl Into<String>,
3588 mut fields: BTreeMap<String, serde_json::Value>,
3589 ) -> Self {
3590 fields.remove("outcome");
3591 Self {
3592 outcome: outcome.into(),
3593 fields,
3594 }
3595 }
3596}
3597
3598impl<'de> Deserialize<'de> for OtherRequestPermissionOutcome {
3599 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3600 where
3601 D: serde::Deserializer<'de>,
3602 {
3603 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
3604 let outcome = fields
3605 .remove("outcome")
3606 .ok_or_else(|| serde::de::Error::missing_field("outcome"))?;
3607 let serde_json::Value::String(outcome) = outcome else {
3608 return Err(serde::de::Error::custom("`outcome` must be a string"));
3609 };
3610
3611 if is_known_request_permission_outcome(&outcome) {
3612 return Err(serde::de::Error::custom(format!(
3613 "known request permission outcome `{outcome}` did not match its schema"
3614 )));
3615 }
3616
3617 Ok(Self { outcome, fields })
3618 }
3619}
3620
3621fn is_known_request_permission_outcome(outcome: &str) -> bool {
3622 matches!(outcome, "cancelled" | "selected")
3623}
3624
3625#[cfg(feature = "schemars")]
3626fn other_request_permission_outcome_schema(schema: &mut Schema) {
3627 super::schema_util::reject_known_string_discriminators(
3628 schema,
3629 "outcome",
3630 &["cancelled", "selected"],
3631 );
3632}
3633
3634#[serde_as]
3636#[skip_serializing_none]
3637#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3638#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3639#[serde(rename_all = "camelCase")]
3640#[non_exhaustive]
3641pub struct CancelledPermissionOutcome {
3642 #[serde_as(deserialize_as = "DefaultOnError")]
3648 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3649 #[serde(default)]
3650 #[serde(rename = "_meta")]
3651 pub meta: Option<Meta>,
3652}
3653
3654impl CancelledPermissionOutcome {
3655 #[must_use]
3657 pub fn new() -> Self {
3658 Self::default()
3659 }
3660
3661 #[must_use]
3667 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3668 self.meta = meta.into_option();
3669 self
3670 }
3671}
3672
3673impl From<CancelledPermissionOutcome> for RequestPermissionOutcome {
3674 fn from(outcome: CancelledPermissionOutcome) -> Self {
3675 Self::Cancelled(outcome)
3676 }
3677}
3678
3679#[serde_as]
3681#[skip_serializing_none]
3682#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3684#[serde(rename_all = "camelCase")]
3685#[non_exhaustive]
3686pub struct SelectedPermissionOutcome {
3687 pub option_id: PermissionOptionId,
3689 #[serde_as(deserialize_as = "DefaultOnError")]
3695 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3696 #[serde(default)]
3697 #[serde(rename = "_meta")]
3698 pub meta: Option<Meta>,
3699}
3700
3701impl SelectedPermissionOutcome {
3702 #[must_use]
3704 pub fn new(option_id: impl Into<PermissionOptionId>) -> Self {
3705 Self {
3706 option_id: option_id.into(),
3707 meta: None,
3708 }
3709 }
3710
3711 #[must_use]
3717 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3718 self.meta = meta.into_option();
3719 self
3720 }
3721}
3722
3723#[serde_as]
3732#[skip_serializing_none]
3733#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3734#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3735#[serde(rename_all = "camelCase")]
3736#[non_exhaustive]
3737pub struct ClientCapabilities {
3738 #[serde_as(deserialize_as = "DefaultOnError")]
3745 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3746 #[serde(default)]
3747 pub auth: Option<ClientAuthCapabilities>,
3748 #[serde_as(deserialize_as = "DefaultOnError")]
3754 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3755 #[serde(default)]
3756 pub elicitation: Option<ElicitationCapabilities>,
3757
3758 #[serde_as(deserialize_as = "DefaultOnError")]
3764 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3765 #[serde(default)]
3766 #[serde(rename = "_meta")]
3767 pub meta: Option<Meta>,
3768}
3769
3770impl ClientCapabilities {
3771 #[must_use]
3773 pub fn new() -> Self {
3774 Self::default()
3775 }
3776
3777 #[must_use]
3781 pub fn auth(mut self, auth: impl IntoOption<ClientAuthCapabilities>) -> Self {
3782 self.auth = auth.into_option();
3783 self
3784 }
3785
3786 #[must_use]
3789 pub fn elicitation(mut self, elicitation: impl IntoOption<ElicitationCapabilities>) -> Self {
3790 self.elicitation = elicitation.into_option();
3791 self
3792 }
3793
3794 #[must_use]
3800 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3801 self.meta = meta.into_option();
3802 self
3803 }
3804}
3805
3806#[serde_as]
3812#[skip_serializing_none]
3813#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3814#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3815#[serde(rename_all = "camelCase")]
3816#[non_exhaustive]
3817pub struct ClientAuthCapabilities {
3818 #[serde_as(deserialize_as = "DefaultOnError")]
3825 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3826 #[serde(default)]
3827 pub terminal: Option<TerminalAuthCapabilities>,
3828 #[serde_as(deserialize_as = "DefaultOnError")]
3834 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3835 #[serde(default)]
3836 #[serde(rename = "_meta")]
3837 pub meta: Option<Meta>,
3838}
3839
3840impl ClientAuthCapabilities {
3841 #[must_use]
3843 pub fn new() -> Self {
3844 Self::default()
3845 }
3846
3847 #[must_use]
3855 pub fn terminal(mut self, terminal: impl IntoOption<TerminalAuthCapabilities>) -> Self {
3856 self.terminal = terminal.into_option();
3857 self
3858 }
3859
3860 #[must_use]
3866 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3867 self.meta = meta.into_option();
3868 self
3869 }
3870}
3871
3872#[serde_as]
3878#[skip_serializing_none]
3879#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3880#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3881#[non_exhaustive]
3882pub struct TerminalAuthCapabilities {
3883 #[serde_as(deserialize_as = "DefaultOnError")]
3889 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
3890 #[serde(default)]
3891 #[serde(rename = "_meta")]
3892 pub meta: Option<Meta>,
3893}
3894
3895impl TerminalAuthCapabilities {
3896 #[must_use]
3898 pub fn new() -> Self {
3899 Self::default()
3900 }
3901
3902 #[must_use]
3908 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
3909 self.meta = meta.into_option();
3910 self
3911 }
3912}
3913
3914#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3920#[non_exhaustive]
3921pub struct ClientMethodNames {
3922 pub session_request_permission: &'static str,
3924 pub session_update: &'static str,
3926 #[cfg(feature = "unstable_mcp_over_acp")]
3928 pub mcp_message: &'static str,
3929 pub elicitation_create: &'static str,
3931 pub elicitation_complete: &'static str,
3933 #[cfg(feature = "unstable_auth_status")]
3935 pub auth_status_update: &'static str,
3936}
3937
3938pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames {
3940 session_update: SESSION_UPDATE_NOTIFICATION,
3941 session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME,
3942 #[cfg(feature = "unstable_mcp_over_acp")]
3943 mcp_message: MCP_MESSAGE_METHOD_NAME,
3944 elicitation_create: ELICITATION_CREATE_METHOD_NAME,
3945 elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION,
3946 #[cfg(feature = "unstable_auth_status")]
3947 auth_status_update: AUTH_STATUS_UPDATE_NOTIFICATION,
3948};
3949
3950pub(crate) const SESSION_UPDATE_NOTIFICATION: &str = "session/update";
3952pub(crate) const SESSION_REQUEST_PERMISSION_METHOD_NAME: &str = "session/request_permission";
3954pub(crate) const ELICITATION_CREATE_METHOD_NAME: &str = "elicitation/create";
3956pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete";
3958#[cfg(feature = "unstable_auth_status")]
3960pub(crate) const AUTH_STATUS_UPDATE_NOTIFICATION: &str = "auth/status_update";
3961
3962#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3969#[derive(Clone, Debug, Serialize, Deserialize)]
3970#[serde(untagged)]
3971#[cfg_attr(feature = "schemars", schemars(inline))]
3972#[non_exhaustive]
3973pub enum AgentRequest {
3974 RequestPermissionRequest(Box<RequestPermissionRequest>),
3985 CreateElicitationRequest(Box<CreateElicitationRequest>),
3989 #[cfg(feature = "unstable_mcp_over_acp")]
3995 MessageMcpRequest(Box<MessageMcpRequest>),
3996 ExtMethodRequest(Box<ExtRequest>),
4004}
4005
4006impl AgentRequest {
4007 #[must_use]
4009 pub fn method(&self) -> &str {
4010 match self {
4011 Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
4012 Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
4013 #[cfg(feature = "unstable_mcp_over_acp")]
4014 Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
4015 Self::ExtMethodRequest(ext_request) => &ext_request.method,
4016 }
4017 }
4018}
4019
4020#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
4027#[derive(Clone, Debug, Serialize, Deserialize)]
4028#[serde(untagged)]
4029#[cfg_attr(feature = "schemars", schemars(inline))]
4030#[non_exhaustive]
4031pub enum ClientResponse {
4032 RequestPermissionResponse(Box<RequestPermissionResponse>),
4034 CreateElicitationResponse(Box<CreateElicitationResponse>),
4036 #[cfg(feature = "unstable_mcp_over_acp")]
4038 MessageMcpResponse(Box<MessageMcpResponse>),
4039 ExtMethodResponse(Box<ExtResponse>),
4041}
4042
4043#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
4050#[derive(Clone, Debug, Serialize, Deserialize)]
4051#[serde(untagged)]
4052#[cfg_attr(feature = "schemars", schemars(inline))]
4053#[non_exhaustive]
4054pub enum AgentNotification {
4055 UpdateSessionNotification(Box<UpdateSessionNotification>),
4068 CompleteElicitationNotification(Box<CompleteElicitationNotification>),
4072 #[cfg(feature = "unstable_auth_status")]
4080 AuthStatusUpdateNotification(Box<AuthStatusUpdateNotification>),
4081 ExtNotification(Box<ExtNotification>),
4089}
4090
4091impl AgentNotification {
4092 #[must_use]
4094 pub fn method(&self) -> &str {
4095 match self {
4096 Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
4097 Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
4098 #[cfg(feature = "unstable_auth_status")]
4099 Self::AuthStatusUpdateNotification(_) => CLIENT_METHOD_NAMES.auth_status_update,
4100 Self::ExtNotification(ext_notification) => &ext_notification.method,
4101 }
4102 }
4103}
4104
4105#[cfg(test)]
4106mod tests {
4107
4108 #[test]
4109 fn background_list_round_trips_on_state_updates() {
4110 use serde_json::json;
4111 let idle = super::StateUpdate::Idle(super::IdleStateUpdate::new().background(vec![
4112 super::BackgroundWork::Terminal(super::BackgroundTerminal::new("term_003")),
4113 super::BackgroundWork::ToolCall(super::BackgroundToolCall::new("call_012")),
4114 ]));
4115 let wire = json!({
4116 "state": "idle",
4117 "background": [
4118 { "type": "terminal", "terminalId": "term_003" },
4119 { "type": "tool_call", "toolCallId": "call_012" }
4120 ]
4121 });
4122 assert_eq!(serde_json::to_value(&idle).unwrap(), wire);
4123 assert_eq!(
4124 serde_json::from_value::<super::StateUpdate>(wire).unwrap(),
4125 idle
4126 );
4127
4128 assert_eq!(idle.background().map(<[_]>::len), Some(2));
4129
4130 let empty = super::RunningStateUpdate::new().background(vec![]);
4131 assert_eq!(
4132 serde_json::to_value(&empty).unwrap(),
4133 json!({ "background": [] })
4134 );
4135 assert_eq!(
4136 super::StateUpdate::Running(empty).background(),
4137 Some(&[][..])
4138 );
4139 assert_eq!(
4140 super::StateUpdate::RequiresAction(super::RequiresActionStateUpdate::new())
4141 .background(),
4142 None
4143 );
4144 let custom: super::StateUpdate = serde_json::from_value(json!({
4145 "state": "_waiting",
4146 "background": [{ "type": "terminal", "terminalId": "term_003" }]
4147 }))
4148 .unwrap();
4149 assert_eq!(custom.background(), None);
4150 }
4151
4152 #[test]
4153 fn background_list_keeps_unknown_items_and_drops_malformed_lists() {
4154 use serde_json::json;
4155 let parsed: super::RunningStateUpdate = serde_json::from_value(json!({
4156 "background": [{ "type": "_cron", "schedule": "* * * * *" }]
4157 }))
4158 .unwrap();
4159 let Some([super::BackgroundWork::Other(other)]) = parsed.background.as_deref() else {
4160 panic!("expected one unknown item: {parsed:?}");
4161 };
4162 assert_eq!(other.kind, "_cron");
4163 assert_eq!(other.fields["schedule"], json!("* * * * *"));
4164
4165 for background in [
4166 json!(null),
4167 json!("oops"),
4168 json!([{ "type": "terminal" }]),
4169 json!([{ "type": "tool_call", "toolCallId": "call_1" }, { "type": "terminal", "terminalId": 7 }]),
4170 ] {
4171 let parsed: super::IdleStateUpdate =
4172 serde_json::from_value(json!({ "background": background })).unwrap();
4173 assert_eq!(parsed.background, None, "{background}");
4174 }
4175 }
4176
4177 #[test]
4178 fn replacement_lists_reject_non_arrays_instead_of_clearing() {
4179 use serde_json::json;
4180 for (update, key) in [
4181 ("config_option_update", "configOptions"),
4182 ("available_commands_update", "availableCommands"),
4183 ] {
4184 let empty = json!({ "sessionUpdate": update, key: [] });
4185 serde_json::from_value::<super::SessionUpdate>(empty).unwrap();
4186 for bad in [json!("oops"), json!(null), json!({})] {
4187 let wire = json!({ "sessionUpdate": update, key: bad });
4188 assert!(
4189 serde_json::from_value::<super::SessionUpdate>(wire).is_err(),
4190 "{update} {bad}"
4191 );
4192 }
4193 }
4194 let plan = |entries: serde_json::Value| {
4195 json!({ "sessionUpdate": "plan_update",
4196 "plan": { "type": "items", "planId": "p", "entries": entries } })
4197 };
4198 serde_json::from_value::<super::SessionUpdate>(plan(json!([]))).unwrap();
4199 assert!(serde_json::from_value::<super::SessionUpdate>(plan(json!("oops"))).is_err());
4200 let skipped =
4202 plan(json!([42, { "content": "a", "priority": "high", "status": "pending" }]));
4203 let super::SessionUpdate::PlanUpdate(update) =
4204 serde_json::from_value::<super::SessionUpdate>(skipped).unwrap()
4205 else {
4206 panic!("expected plan_update");
4207 };
4208 let crate::v2::PlanUpdateContent::Items(items) = update.plan else {
4209 panic!("expected items");
4210 };
4211 assert_eq!(items.entries.len(), 1);
4212 }
4213
4214 #[test]
4215 fn other_available_command_preserves_meta_verbatim() {
4216 for meta in [serde_json::json!({ "k": 1 }), serde_json::json!("bad")] {
4217 let wire = serde_json::json!({ "type": "_custom", "name": "c",
4218 "description": "d", "_meta": meta, "extra": true });
4219 let command: super::AvailableCommand = serde_json::from_value(wire.clone()).unwrap();
4220 assert_eq!(command.meta(), meta.as_object());
4221 assert_eq!(serde_json::to_value(&command).unwrap(), wire);
4222 }
4223 }
4224
4225 #[test]
4226 fn cancelled_permission_outcome_wire_format() {
4227 let outcome: RequestPermissionOutcome =
4228 serde_json::from_value(serde_json::json!({ "outcome": "cancelled" })).unwrap();
4229 assert_eq!(outcome, CancelledPermissionOutcome::new().into());
4230 assert_eq!(
4231 serde_json::to_value(&outcome).unwrap(),
4232 serde_json::json!({ "outcome": "cancelled" })
4233 );
4234
4235 let with_meta: RequestPermissionOutcome = CancelledPermissionOutcome::new()
4236 .meta(serde_json::Map::from_iter([(
4237 "k".to_string(),
4238 serde_json::json!(1),
4239 )]))
4240 .into();
4241 let json = serde_json::to_value(&with_meta).unwrap();
4242 assert_eq!(
4243 json,
4244 serde_json::json!({ "outcome": "cancelled", "_meta": { "k": 1 } })
4245 );
4246 assert_eq!(
4247 serde_json::from_value::<RequestPermissionOutcome>(json).unwrap(),
4248 with_meta
4249 );
4250 }
4251
4252 use super::*;
4253
4254 #[cfg(feature = "unstable_subagents")]
4255 #[test]
4256 fn subagent_update_serializes_as_upsert() {
4257 use serde_json::json;
4258
4259 let announced =
4260 SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
4261 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
4262 ));
4263 let wire = json!({
4264 "sessionUpdate": "subagent_update",
4265 "sessionId": "sess_child_1",
4266 "capabilities": { "cancel": {} }
4267 });
4268 assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
4269 assert_eq!(
4270 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4271 announced
4272 );
4273
4274 let minimal = SubagentUpdate::new("sess_child_1");
4275 assert_eq!(
4276 serde_json::to_value(&minimal).unwrap(),
4277 json!({
4278 "sessionId": "sess_child_1"
4279 })
4280 );
4281 assert!(minimal.capabilities.is_undefined());
4282 assert!(minimal.state.is_undefined());
4283 assert!(minimal.meta.is_undefined());
4284
4285 let cleared_wire = json!({
4287 "sessionId": "sess_child_1",
4288 "capabilities": null,
4289 "state": null,
4290 "_meta": null
4291 });
4292 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4293 assert!(cleared.capabilities.is_null());
4294 assert!(cleared.state.is_null());
4295 assert!(cleared.meta.is_null());
4296 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4297
4298 let revoked: SubagentUpdate = serde_json::from_value(json!({
4299 "sessionId": "sess_child_1",
4300 "capabilities": {}
4301 }))
4302 .unwrap();
4303 assert_eq!(
4304 revoked.capabilities,
4305 MaybeUndefined::Value(SubagentSessionCapabilities::new())
4306 );
4307 assert!(
4308 matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
4309 );
4310
4311 assert!(
4312 serde_json::from_value::<SessionUpdate>(json!({
4313 "sessionUpdate": "subagent_update"
4314 }))
4315 .is_err()
4316 );
4317 }
4318
4319 #[cfg(feature = "unstable_subagents")]
4320 #[test]
4321 fn subagent_cancel_capability_is_optional_object() {
4322 use serde_json::json;
4323
4324 for wire in [
4325 json!({}),
4326 json!({"cancel": null}),
4327 json!({"cancel": true}),
4328 json!({"cancel": false}),
4329 ] {
4330 let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
4331 assert!(child.cancel.is_none());
4332 assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
4333 }
4334 let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
4335 assert_eq!(
4336 serde_json::to_value(&enabled).unwrap(),
4337 json!({"cancel": {}})
4338 );
4339 assert_eq!(
4340 serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
4341 enabled
4342 );
4343 let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
4344 let with_meta =
4345 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
4346 let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
4347 assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
4348 assert_eq!(
4349 serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
4350 with_meta
4351 );
4352 assert!(enabled.cancel.is_some());
4353 assert!(enabled.cancel(None).cancel.is_none());
4354 let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
4355 assert_eq!(
4356 serde_json::to_value(removed).unwrap(),
4357 json!({"sessionId": "child", "capabilities": {}})
4358 );
4359 }
4360
4361 #[cfg(feature = "unstable_subagents")]
4362 #[test]
4363 fn subagent_display_metadata_uses_patch_semantics() {
4364 use serde_json::json;
4365
4366 let update = SubagentUpdate::new("child")
4367 .title("Test investigator".to_string())
4368 .description("Investigates platform-specific test failures.".to_string());
4369 let wire = json!({
4370 "sessionId": "child",
4371 "title": "Test investigator",
4372 "description": "Investigates platform-specific test failures."
4373 });
4374 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4375 assert_eq!(
4376 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4377 update
4378 );
4379
4380 let minimal = SubagentUpdate::new("child");
4381 assert!(minimal.title.is_undefined());
4382 assert!(minimal.description.is_undefined());
4383 assert_eq!(
4384 serde_json::to_value(&minimal).unwrap(),
4385 json!({"sessionId": "child"})
4386 );
4387 let cleared_wire = json!({
4388 "sessionId": "child", "title": null, "description": null
4389 });
4390 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4391 assert!(cleared.title.is_null());
4392 assert!(cleared.description.is_null());
4393 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4394
4395 let title_only: SubagentUpdate = serde_json::from_value(json!({
4396 "sessionId": "child", "title": "Updated title"
4397 }))
4398 .unwrap();
4399 assert_eq!(
4400 title_only.title,
4401 MaybeUndefined::Value("Updated title".to_string())
4402 );
4403 assert!(title_only.description.is_undefined());
4404 let malformed: SubagentUpdate = serde_json::from_value(json!({
4405 "sessionId": "child", "title": false, "description": false
4406 }))
4407 .unwrap();
4408 assert_eq!(malformed, minimal);
4409 }
4410
4411 #[cfg(feature = "unstable_subagents")]
4412 #[test]
4413 fn subagent_directories_use_strict_patch_semantics() {
4414 use serde_json::json;
4415
4416 let update = SubagentUpdate::new("child")
4417 .cwd("/home/user/project/.worktrees/investigator")
4418 .additional_directories(vec![]);
4419 let wire = json!({
4420 "sessionId": "child",
4421 "cwd": "/home/user/project/.worktrees/investigator",
4422 "additionalDirectories": []
4423 });
4424 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4425 assert_eq!(
4426 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4427 update
4428 );
4429
4430 let inherited: SubagentUpdate =
4431 serde_json::from_value(json!({"sessionId": "child"})).unwrap();
4432 assert!(inherited.cwd.is_undefined());
4433 assert!(inherited.additional_directories.is_undefined());
4434
4435 let cleared_wire = json!({
4436 "sessionId": "child", "cwd": null, "additionalDirectories": null
4437 });
4438 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4439 assert!(cleared.cwd.is_null());
4440 assert!(cleared.additional_directories.is_null());
4441 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4442
4443 for malformed in [
4444 json!({"sessionId": "child", "cwd": false}),
4445 json!({"sessionId": "child", "additionalDirectories": "/tmp/wt"}),
4446 json!({"sessionId": "child", "additionalDirectories": ["/tmp/wt", false]}),
4447 ] {
4448 assert!(
4449 serde_json::from_value::<SubagentUpdate>(malformed.clone()).is_err(),
4450 "{malformed}"
4451 );
4452 }
4453 }
4454
4455 #[cfg(feature = "unstable_subagents")]
4456 #[test]
4457 fn subagent_notification_keeps_parent_and_child_ids_nested() {
4458 use serde_json::json;
4459
4460 let notification = UpdateSessionNotification::new(
4461 "parent",
4462 SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
4463 );
4464 let wire = json!({
4465 "sessionId": "parent",
4466 "update": {
4467 "sessionUpdate": "subagent_update",
4468 "sessionId": "child"
4469 }
4470 });
4471 assert_eq!(serde_json::to_value(¬ification).unwrap(), wire);
4472 assert_eq!(
4473 serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
4474 notification
4475 );
4476 for malformed in [
4477 json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
4478 json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
4479 ] {
4480 assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
4481 }
4482 }
4483
4484 #[cfg(feature = "unstable_subagents")]
4485 #[test]
4486 fn subagent_mirrors_normal_child_state_notifications() {
4487 use serde_json::json;
4488
4489 let association = SubagentUpdate::new("sess_child");
4490 for state in [
4491 StateUpdate::Running(RunningStateUpdate::new()),
4492 StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
4493 StateUpdate::Unknown(UnknownStateUpdate::new()),
4494 StateUpdate::Running(RunningStateUpdate::new()),
4495 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
4496 StateUpdate::Running(RunningStateUpdate::new()),
4497 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
4498 ] {
4499 let child_notification = UpdateSessionNotification::new(
4500 association.session_id.clone(),
4501 SessionUpdate::StateUpdate(state.clone()),
4502 );
4503 let parent_notification = UpdateSessionNotification::new(
4504 "sess_parent",
4505 SessionUpdate::SubagentUpdate(association.clone().state(state)),
4506 );
4507 let child_wire = serde_json::to_value(&child_notification).unwrap();
4508 let parent_wire = serde_json::to_value(&parent_notification).unwrap();
4509 assert_eq!(child_wire["sessionId"], json!("sess_child"));
4510 assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
4511 assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
4512 assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
4513 let mut child_snapshot = child_wire["update"].clone();
4514 child_snapshot
4515 .as_object_mut()
4516 .unwrap()
4517 .remove("sessionUpdate");
4518 assert_eq!(parent_wire["update"]["state"], child_snapshot);
4519 assert_eq!(
4520 serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
4521 child_notification
4522 );
4523 assert_eq!(
4524 serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
4525 parent_notification
4526 );
4527 }
4528 }
4529
4530 #[cfg(feature = "unstable_subagents")]
4531 #[test]
4532 fn subagent_state_uses_whole_snapshot_patch_semantics() {
4533 use serde_json::json;
4534
4535 let minimal = SubagentUpdate::new("child");
4536 assert!(minimal.state.is_undefined());
4537 assert_eq!(
4538 serde_json::to_value(&minimal).unwrap(),
4539 json!({"sessionId": "child"})
4540 );
4541
4542 let cleared = minimal.clone().state(None::<StateUpdate>);
4543 assert!(cleared.state.is_null());
4544 assert_eq!(
4545 serde_json::to_value(&cleared).unwrap(),
4546 json!({"sessionId": "child", "state": null})
4547 );
4548 assert_eq!(
4549 serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
4550 .unwrap(),
4551 cleared
4552 );
4553
4554 for snapshot in [
4555 json!({"state": "running", "_meta": {"source": "worker"}}),
4556 json!({"state": "idle", "stopReason": "end_turn"}),
4557 json!({"state": "requires_action"}),
4558 json!({"state": "unknown"}),
4559 json!({"state": "_custom", "detail": {"phase": 2}}),
4560 ] {
4561 let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
4562 let update = minimal.clone().state(state.clone());
4563 let wire = json!({"sessionId": "child", "state": snapshot});
4564 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4565 assert_eq!(
4566 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4567 update
4568 );
4569 assert_eq!(update.state, MaybeUndefined::Value(state));
4570 }
4571
4572 let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
4575 IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
4576 ));
4577 let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
4578 assert_eq!(
4579 serde_json::to_value(running).unwrap(),
4580 json!({"sessionId": "child", "state": {"state": "running"}})
4581 );
4582 for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
4583 let parsed: SubagentUpdate =
4584 serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
4585 assert_eq!(parsed, minimal);
4586 }
4587 }
4588
4589 #[cfg(feature = "unstable_subagents")]
4590 #[test]
4591 fn unknown_activity_is_a_known_state_update() {
4592 use serde_json::json;
4593
4594 let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
4595 UnknownStateUpdate::new().meta(
4596 [("source".into(), json!("worker"))]
4597 .into_iter()
4598 .collect::<Meta>(),
4599 ),
4600 ));
4601 let wire = json!({
4602 "sessionUpdate": "state_update",
4603 "state": "unknown",
4604 "_meta": { "source": "worker" }
4605 });
4606 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4607 assert_eq!(
4608 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4609 update
4610 );
4611
4612 for meta in [json!(null), json!(false)] {
4613 let state: StateUpdate = serde_json::from_value(json!({
4614 "state": "unknown",
4615 "_meta": meta
4616 }))
4617 .unwrap();
4618 assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
4619 assert_eq!(
4620 serde_json::to_value(state).unwrap(),
4621 json!({"state": "unknown"})
4622 );
4623 }
4624 assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
4625 }
4626
4627 #[cfg(not(feature = "unstable_subagents"))]
4628 #[test]
4629 fn unknown_activity_is_preserved_without_subagents_feature() {
4630 let wire = serde_json::json!({
4631 "sessionUpdate": "state_update",
4632 "state": "unknown",
4633 "_meta": { "source": "worker" }
4634 });
4635 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4636 let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
4637 panic!("expected unrecognized state payload");
4638 };
4639 assert_eq!(state.state, "unknown");
4640 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4641 }
4642
4643 #[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
4644 #[test]
4645 fn subagent_schema_carries_optional_nullable_state_snapshot() {
4646 use serde_json::json;
4647
4648 let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
4649 let properties = schema["properties"].as_object().unwrap();
4650 assert!(properties.contains_key("sessionId"));
4651 assert!(properties.contains_key("title"));
4652 assert!(properties.contains_key("description"));
4653 assert!(properties.contains_key("capabilities"));
4654 assert!(properties.contains_key("state"));
4655 assert!(properties.contains_key("_meta"));
4656 assert!(!properties.contains_key("name"));
4657 assert!(!properties.contains_key("task"));
4658 assert_eq!(schema["required"], json!(["sessionId"]));
4659 assert_eq!(
4660 properties["state"]["x-deserialize-default-on-error"],
4661 json!(true)
4662 );
4663 let state_variants = properties["state"]["anyOf"].as_array().unwrap();
4664 assert!(state_variants.contains(&json!({"type": "null"})));
4665 assert!(
4666 state_variants
4667 .iter()
4668 .any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
4669 );
4670 let child =
4671 serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
4672 let variants = child["properties"]["cancel"]["anyOf"]
4673 .as_array()
4674 .expect("cancel must allow the capability object or null");
4675 assert!(variants.contains(&json!({"type": "null"})));
4676 assert!(
4677 variants
4678 .iter()
4679 .any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
4680 );
4681 assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
4682 let cancel =
4683 serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
4684 assert_eq!(cancel["type"], "object");
4685 assert!(
4686 !child["required"]
4687 .as_array()
4688 .is_some_and(|required| required.contains(&json!("cancel")))
4689 );
4690 }
4691
4692 #[cfg(not(feature = "unstable_subagents"))]
4693 #[test]
4694 fn unsupported_subagent_update_is_preserved() {
4695 let wire = serde_json::json!({
4696 "sessionUpdate": "subagent_update",
4697 "sessionId": "sess_child",
4698 "capabilities": { "cancel": {} }
4699 });
4700 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4701 assert!(matches!(&parsed, SessionUpdate::Other(_)));
4702 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4703 }
4704
4705 #[test]
4706 fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
4707 use serde_json::json;
4708
4709 let mut meta = Meta::new();
4710 meta.insert("source".into(), json!("fallback"));
4711 let v2_notice = SessionUpdate::Notice(
4712 Notice::new(NoticeSeverity::Error, "Provider degraded")
4713 .description("Requests may take longer than usual.")
4714 .meta(meta.clone()),
4715 );
4716 let expected = json!({
4717 "sessionUpdate": "notice",
4718 "severity": "error",
4719 "title": "Provider degraded",
4720 "description": "Requests may take longer than usual.",
4721 "_meta": { "source": "fallback" }
4722 });
4723 assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
4724
4725 let v1_notice = crate::v1::SessionUpdate::Notice(
4726 crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
4727 .description("Requests may take longer than usual.")
4728 .meta(meta),
4729 );
4730 assert_eq!(
4731 serde_json::to_value(v2_notice).unwrap(),
4732 serde_json::to_value(v1_notice).unwrap()
4733 );
4734
4735 let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
4736 "sessionUpdate": "notice",
4737 "severity": "critical",
4738 "title": "Provider degraded",
4739 "description": null,
4740 "_meta": null
4741 }))
4742 .unwrap() else {
4743 panic!("expected notice");
4744 };
4745
4746 assert_eq!(
4747 notice.severity,
4748 NoticeSeverity::Other("critical".to_string())
4749 );
4750 assert_eq!(notice.description, None);
4751 assert_eq!(notice.meta, None);
4752 assert_eq!(
4753 serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
4754 json!({
4755 "sessionUpdate": "notice",
4756 "severity": "critical",
4757 "title": "Provider degraded"
4758 })
4759 );
4760 }
4761
4762 #[test]
4763 fn malformed_known_notice_is_not_hidden_as_unknown() {
4764 use serde_json::json;
4765
4766 for malformed in [
4767 json!({
4768 "sessionUpdate": "notice",
4769 "severity": "warning",
4770 "title": 42
4771 }),
4772 json!({
4773 "sessionUpdate": "notice",
4774 "severity": 42,
4775 "title": "MCP server unavailable"
4776 }),
4777 json!({
4778 "sessionUpdate": "notice",
4779 "severity": "warning"
4780 }),
4781 json!({
4782 "sessionUpdate": "notice",
4783 "severity": "warning",
4784 "title": null
4785 }),
4786 json!({
4787 "sessionUpdate": "notice",
4788 "title": "MCP server unavailable"
4789 }),
4790 json!({
4791 "sessionUpdate": "notice",
4792 "severity": null,
4793 "title": "MCP server unavailable"
4794 }),
4795 ] {
4796 assert!(serde_json::from_str::<SessionUpdate>(&malformed.to_string()).is_err());
4797 assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
4798 }
4799 }
4800
4801 #[test]
4802 fn notice_tolerates_invalid_optional_display_fields_and_custom_severity() {
4803 use serde_json::json;
4804
4805 let wire = json!({
4806 "sessionUpdate": "notice",
4807 "severity": "_custom",
4808 "title": "Provider degraded",
4809 "description": 42,
4810 "_meta": false
4811 });
4812 let SessionUpdate::Notice(notice) = serde_json::from_value(wire.clone()).unwrap() else {
4813 panic!("expected notice");
4814 };
4815 assert_eq!(
4816 serde_json::from_str::<SessionUpdate>(&wire.to_string()).unwrap(),
4817 SessionUpdate::Notice(notice.clone())
4818 );
4819 assert_eq!(notice.severity, NoticeSeverity::Other("_custom".into()));
4820 assert_eq!(notice.description, None);
4821 assert_eq!(notice.meta, None);
4822 assert_eq!(
4823 serde_json::to_value(notice).unwrap(),
4824 json!({"severity": "_custom", "title": "Provider degraded"})
4825 );
4826 }
4827
4828 #[cfg(feature = "schemars")]
4829 #[test]
4830 fn notice_schema_preserves_required_fields_and_excludes_unknown_fallback() {
4831 use serde_json::json;
4832
4833 let schema = serde_json::to_value(schemars::schema_for!(Notice)).unwrap();
4834 assert_eq!(schema["required"], json!(["severity", "title"]));
4835 assert_eq!(schema["properties"]["title"]["minLength"], 1);
4836 for field in ["description", "_meta"] {
4837 assert!(
4838 schema["properties"][field]["type"]
4839 .as_array()
4840 .unwrap()
4841 .contains(&json!("null"))
4842 );
4843 }
4844 let fallback = serde_json::to_value(schemars::schema_for!(OtherSessionUpdate)).unwrap();
4845 assert!(
4846 fallback["not"]["anyOf"]
4847 .as_array()
4848 .unwrap()
4849 .iter()
4850 .any(|variant| variant["properties"]["sessionUpdate"]["const"] == "notice")
4851 );
4852 }
4853
4854 #[test]
4855 fn compaction_updates_preserve_patch_and_open_status_semantics() {
4856 use serde_json::json;
4857
4858 assert_eq!(
4859 serde_json::to_value(SessionUpdate::CompactionUpdate(
4860 CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
4861 ContentBlock::Text(crate::v2::TextContent::new("retained")),
4862 ]),
4863 ))
4864 .unwrap(),
4865 json!({
4866 "sessionUpdate": "compaction_update",
4867 "compactionId": "cmp_001",
4868 "status": "completed",
4869 "summary": [{ "type": "text", "text": "retained" }]
4870 })
4871 );
4872
4873 let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
4874 "sessionUpdate": "compaction_update",
4875 "compactionId": "cmp_001",
4876 "status": "paused",
4877 "summary": null
4878 }))
4879 .unwrap() else {
4880 panic!("expected compaction update");
4881 };
4882 assert_eq!(update.status, CompactionStatus::Other("paused".into()));
4883 assert!(update.summary.is_null());
4884 assert!(update.error.is_undefined());
4885 }
4886
4887 #[test]
4888 fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
4889 use serde_json::json;
4890
4891 assert!(
4892 serde_json::from_value::<SessionUpdate>(json!({
4893 "sessionUpdate": "compaction_update",
4894 "status": "completed"
4895 }))
4896 .is_err()
4897 );
4898 assert!(
4899 serde_json::from_value::<SessionUpdate>(json!({
4900 "sessionUpdate": "compaction_summary_chunk",
4901 "compactionId": "cmp_001"
4902 }))
4903 .is_err()
4904 );
4905 }
4906
4907 #[test]
4908 fn test_elicitation_capability_semantics() {
4909 use serde_json::json;
4910
4911 let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
4912 assert!(unsupported.elicitation.is_none());
4913
4914 let null: ClientCapabilities =
4915 serde_json::from_value(json!({ "elicitation": null })).unwrap();
4916 assert!(null.elicitation.is_none());
4917
4918 let malformed: ClientCapabilities =
4919 serde_json::from_value(json!({ "elicitation": false })).unwrap();
4920 assert!(malformed.elicitation.is_none());
4921
4922 let empty: ClientCapabilities =
4923 serde_json::from_value(json!({ "elicitation": {} })).unwrap();
4924 let empty = empty.elicitation.expect("present capability");
4925 assert!(!empty.supports_form());
4926 assert!(!empty.supports_url());
4927
4928 let form_only: ClientCapabilities = serde_json::from_value(json!({
4929 "elicitation": { "form": {} }
4930 }))
4931 .unwrap();
4932 let form_only = form_only.elicitation.expect("advertised capability");
4933 assert!(form_only.supports_form());
4934 assert!(!form_only.supports_url());
4935
4936 let url_only: ClientCapabilities = serde_json::from_value(json!({
4937 "elicitation": { "url": {} }
4938 }))
4939 .unwrap();
4940 let url_only = url_only.elicitation.expect("advertised capability");
4941 assert!(!url_only.supports_form());
4942 assert!(url_only.supports_url());
4943
4944 let both: ClientCapabilities = serde_json::from_value(json!({
4945 "elicitation": { "form": {}, "url": {} }
4946 }))
4947 .unwrap();
4948 let both = both.elicitation.expect("advertised capability");
4949 assert!(both.supports_form());
4950 assert!(both.supports_url());
4951 }
4952
4953 #[test]
4954 fn test_elicitation_method_routing_and_envelopes() {
4955 use serde_json::json;
4956
4957 assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
4958 assert_eq!(
4959 CLIENT_METHOD_NAMES.elicitation_complete,
4960 "elicitation/complete"
4961 );
4962
4963 let request =
4964 AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
4965 crate::v2::ElicitationFormMode::new(
4966 crate::v2::ElicitationSessionScope::new("sess_1"),
4967 crate::v2::ElicitationSchema::new(),
4968 ),
4969 "Choose a value",
4970 )));
4971 assert_eq!(request.method(), "elicitation/create");
4972 let method = Arc::from(request.method());
4973 let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
4974 id: crate::v2::RequestId::Number(7),
4975 method,
4976 params: Some(request),
4977 });
4978 assert_eq!(
4979 serde_json::to_value(request).unwrap(),
4980 json!({
4981 "jsonrpc": "2.0",
4982 "id": 7,
4983 "method": "elicitation/create",
4984 "params": {
4985 "mode": "form",
4986 "sessionId": "sess_1",
4987 "message": "Choose a value",
4988 "requestedSchema": { "type": "object", "properties": {} }
4989 }
4990 })
4991 );
4992
4993 let notification = AgentNotification::CompleteElicitationNotification(Box::new(
4994 CompleteElicitationNotification::new("elic_1"),
4995 ));
4996 assert_eq!(notification.method(), "elicitation/complete");
4997 let method = Arc::from(notification.method());
4998 let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
4999 method,
5000 params: Some(notification),
5001 });
5002 assert_eq!(
5003 serde_json::to_value(notification).unwrap(),
5004 json!({
5005 "jsonrpc": "2.0",
5006 "method": "elicitation/complete",
5007 "params": { "elicitationId": "elic_1" }
5008 })
5009 );
5010 }
5011
5012 #[test]
5013 fn test_client_capabilities_auth_defaults_on_malformed_value() {
5014 use serde_json::json;
5015
5016 let capabilities: ClientCapabilities = serde_json::from_value(json!({
5017 "auth": false
5018 }))
5019 .unwrap();
5020
5021 assert_eq!(capabilities.auth, None);
5022 }
5023
5024 #[test]
5025 fn test_serialization_behavior() {
5026 use serde_json::json;
5027
5028 assert_eq!(
5029 serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
5030 SessionInfoUpdate {
5031 title: MaybeUndefined::Undefined,
5032 updated_at: MaybeUndefined::Undefined,
5033 meta: MaybeUndefined::Undefined
5034 }
5035 );
5036 assert_eq!(
5037 serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
5038 .unwrap(),
5039 SessionInfoUpdate {
5040 title: MaybeUndefined::Null,
5041 updated_at: MaybeUndefined::Null,
5042 meta: MaybeUndefined::Undefined
5043 }
5044 );
5045 assert_eq!(
5046 serde_json::from_value::<SessionInfoUpdate>(
5047 json!({"title": "title", "updatedAt": "timestamp"})
5048 )
5049 .unwrap(),
5050 SessionInfoUpdate {
5051 title: MaybeUndefined::Value("title".to_string()),
5052 updated_at: MaybeUndefined::Value("timestamp".to_string()),
5053 meta: MaybeUndefined::Undefined
5054 }
5055 );
5056
5057 let clear_meta =
5058 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
5059 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
5060
5061 let mut meta = Meta::new();
5062 meta.insert("source".to_string(), json!("session-info"));
5063
5064 assert_eq!(
5065 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
5066 "source": "session-info"
5067 }}))
5068 .unwrap()
5069 .meta,
5070 MaybeUndefined::Value(meta.clone())
5071 );
5072
5073 assert_eq!(
5074 serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
5075 json!({})
5076 );
5077
5078 assert_eq!(
5079 serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
5080 json!({"_meta": null})
5081 );
5082
5083 assert_eq!(
5084 serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
5085 json!({"_meta": {
5086 "source": "session-info"
5087 }})
5088 );
5089 assert_eq!(
5090 serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
5091 json!({"title": "title"})
5092 );
5093 assert_eq!(
5094 serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
5095 json!({"title": null})
5096 );
5097 assert_eq!(
5098 serde_json::to_value(
5099 SessionInfoUpdate::new()
5100 .title("title")
5101 .title(MaybeUndefined::Undefined)
5102 )
5103 .unwrap(),
5104 json!({})
5105 );
5106 }
5107
5108 #[test]
5109 fn test_content_chunk_message_id_serialization() {
5110 use serde_json::json;
5111
5112 assert_eq!(
5113 serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
5114 ContentBlock::Text(crate::v2::TextContent::new("Hello")),
5115 "msg_agent_c42b9",
5116 )))
5117 .unwrap(),
5118 json!({
5119 "sessionUpdate": "agent_message_chunk",
5120 "messageId": "msg_agent_c42b9",
5121 "content": {
5122 "type": "text",
5123 "text": "Hello"
5124 }
5125 })
5126 );
5127
5128 let err = serde_json::from_value::<ContentChunk>(json!({
5129 "content": {
5130 "type": "text",
5131 "text": "Hello"
5132 }
5133 }))
5134 .unwrap_err();
5135
5136 assert!(err.to_string().contains("messageId"), "{err}");
5137 }
5138
5139 #[test]
5140 fn test_tool_call_content_chunk_serialization() {
5141 use serde_json::json;
5142
5143 assert_eq!(
5144 serde_json::to_value(SessionUpdate::ToolCallContentChunk(
5145 ToolCallContentChunk::new(
5146 "call_001",
5147 crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
5148 )
5149 ))
5150 .unwrap(),
5151 json!({
5152 "sessionUpdate": "tool_call_content_chunk",
5153 "toolCallId": "call_001",
5154 "content": {
5155 "type": "content",
5156 "content": {
5157 "type": "text",
5158 "text": "partial output"
5159 }
5160 }
5161 })
5162 );
5163
5164 let err = serde_json::from_value::<ToolCallContentChunk>(json!({
5165 "content": {
5166 "type": "content",
5167 "content": {
5168 "type": "text",
5169 "text": "partial output"
5170 }
5171 }
5172 }))
5173 .unwrap_err();
5174
5175 assert!(err.to_string().contains("toolCallId"), "{err}");
5176 }
5177
5178 #[test]
5179 fn test_full_message_serialization() {
5180 use serde_json::json;
5181
5182 assert_eq!(
5183 serde_json::to_value(SessionUpdate::UserMessage(
5184 UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
5185 crate::v2::TextContent::new("Hello")
5186 )])
5187 ))
5188 .unwrap(),
5189 json!({
5190 "sessionUpdate": "user_message",
5191 "messageId": "msg_user_8f7a1",
5192 "content": [
5193 {
5194 "type": "text",
5195 "text": "Hello"
5196 }
5197 ]
5198 })
5199 );
5200
5201 assert_eq!(
5202 serde_json::to_value(SessionUpdate::AgentMessage(
5203 AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
5204 crate::v2::TextContent::new("Hello")
5205 )])
5206 ))
5207 .unwrap(),
5208 json!({
5209 "sessionUpdate": "agent_message",
5210 "messageId": "msg_agent_c42b9",
5211 "content": [
5212 {
5213 "type": "text",
5214 "text": "Hello"
5215 }
5216 ]
5217 })
5218 );
5219
5220 assert_eq!(
5221 serde_json::to_value(SessionUpdate::AgentThought(
5222 AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
5223 crate::v2::TextContent::new("Need to inspect the call sites first.")
5224 )])
5225 ))
5226 .unwrap(),
5227 json!({
5228 "sessionUpdate": "agent_thought",
5229 "messageId": "msg_thought_a12",
5230 "content": [
5231 {
5232 "type": "text",
5233 "text": "Need to inspect the call sites first."
5234 }
5235 ]
5236 })
5237 );
5238 }
5239
5240 #[test]
5241 fn test_message_upsert_serialization() {
5242 use serde_json::json;
5243
5244 assert_eq!(
5245 serde_json::to_value(SessionUpdate::UserMessage(
5246 UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
5247 ))
5248 .unwrap(),
5249 json!({
5250 "sessionUpdate": "user_message",
5251 "messageId": "msg_empty",
5252 "content": []
5253 })
5254 );
5255
5256 let empty = serde_json::from_value::<UserMessage>(json!({
5257 "messageId": "msg_empty",
5258 "content": []
5259 }))
5260 .unwrap();
5261 assert!(matches!(
5262 empty.content,
5263 MaybeUndefined::Value(ref content) if content.is_empty()
5264 ));
5265
5266 let patch = serde_json::from_value::<AgentMessage>(json!({
5267 "messageId": "msg_agent_c42b9"
5268 }))
5269 .unwrap();
5270 assert_eq!(patch.content, MaybeUndefined::Undefined);
5271 assert_eq!(patch.meta, MaybeUndefined::Undefined);
5272
5273 let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
5274 "messageId": "msg_agent_c42b9",
5275 "_meta": false
5276 }))
5277 .unwrap();
5278 assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
5279
5280 let patch = serde_json::from_value::<AgentThought>(json!({
5281 "messageId": "msg_thought_a12"
5282 }))
5283 .unwrap();
5284 assert_eq!(patch.content, MaybeUndefined::Undefined);
5285
5286 let clear = serde_json::from_value::<UserMessage>(json!({
5287 "messageId": "msg_user_8f7a1",
5288 "content": null
5289 }))
5290 .unwrap();
5291 assert_eq!(clear.content, MaybeUndefined::Null);
5292
5293 let clear_meta = serde_json::from_value::<UserMessage>(json!({
5294 "messageId": "msg_user_8f7a1",
5295 "_meta": null
5296 }))
5297 .unwrap();
5298 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
5299
5300 let mut meta = Meta::new();
5301 meta.insert("source".to_string(), json!("replay"));
5302
5303 assert_eq!(
5304 serde_json::to_value(SessionUpdate::UserMessage(
5305 UserMessage::new("msg_user_8f7a1").meta(meta)
5306 ))
5307 .unwrap(),
5308 json!({
5309 "sessionUpdate": "user_message",
5310 "messageId": "msg_user_8f7a1",
5311 "_meta": {
5312 "source": "replay"
5313 }
5314 })
5315 );
5316
5317 assert_eq!(
5318 serde_json::to_value(SessionUpdate::UserMessage(
5319 UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
5320 ))
5321 .unwrap(),
5322 json!({
5323 "sessionUpdate": "user_message",
5324 "messageId": "msg_user_8f7a1",
5325 "_meta": null
5326 })
5327 );
5328 }
5329
5330 #[test]
5331 fn test_usage_update_serialization() {
5332 use serde_json::json;
5333
5334 assert_eq!(
5335 serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
5336 53_000, 200_000
5337 )))
5338 .unwrap(),
5339 json!({
5340 "sessionUpdate": "usage_update",
5341 "used": 53000,
5342 "size": 200_000
5343 })
5344 );
5345
5346 assert_eq!(
5347 serde_json::to_value(SessionUpdate::UsageUpdate(
5348 UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
5349 ))
5350 .unwrap(),
5351 json!({
5352 "sessionUpdate": "usage_update",
5353 "used": 53000,
5354 "size": 200_000,
5355 "cost": {
5356 "amount": 0.045,
5357 "currency": "USD"
5358 }
5359 })
5360 );
5361
5362 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
5363 "sessionUpdate": "usage_update",
5364 "used": 53000,
5365 "size": 200_000,
5366 "cost": null
5367 }))
5368 .unwrap() else {
5369 panic!("expected usage update");
5370 };
5371
5372 assert_eq!(update.cost, MaybeUndefined::Null);
5373
5374 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
5375 "sessionUpdate": "usage_update",
5376 "used": 53000,
5377 "size": 200_000
5378 }))
5379 .unwrap() else {
5380 panic!("expected usage update");
5381 };
5382
5383 assert_eq!(update.cost, MaybeUndefined::Undefined);
5384 }
5385
5386 #[test]
5387 fn error_stop_reason_carries_failure_beside_discriminator() {
5388 use crate::v2::{Error, ErrorStopReason};
5389 use serde_json::json;
5390
5391 let update =
5392 SessionUpdate::StateUpdate(StateUpdate::Idle(IdleStateUpdate::new().stop_reason(
5393 StopReason::from(ErrorStopReason::new().error(Error::auth_required())),
5394 )));
5395 let wire = json!({
5396 "sessionUpdate": "state_update",
5397 "state": "idle",
5398 "stopReason": "error",
5399 "error": { "code": -32000, "message": "Authentication required" }
5400 });
5401 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
5402 assert_eq!(
5403 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
5404 update
5405 );
5406
5407 let with_data =
5408 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::from(
5409 ErrorStopReason::new().error(
5410 Error::new(-32099, "Provider unavailable").data(json!({ "status": 503 })),
5411 ),
5412 )));
5413 let wire = json!({
5414 "state": "idle",
5415 "stopReason": "error",
5416 "error": {
5417 "code": -32099,
5418 "message": "Provider unavailable",
5419 "data": { "status": 503 }
5420 }
5421 });
5422 assert_eq!(serde_json::to_value(&with_data).unwrap(), wire);
5423 assert_eq!(
5424 serde_json::from_value::<StateUpdate>(wire).unwrap(),
5425 with_data
5426 );
5427
5428 let without_details = StateUpdate::Idle(
5431 IdleStateUpdate::new().stop_reason(StopReason::from(ErrorStopReason::new())),
5432 );
5433 for error in [
5434 None,
5435 Some(json!(null)),
5436 Some(json!("failed")),
5437 Some(json!({ "message": "missing code" })),
5438 ] {
5439 let mut wire = json!({ "state": "idle", "stopReason": "error" });
5440 if let Some(error) = error {
5441 wire["error"] = error;
5442 }
5443 assert_eq!(
5444 serde_json::from_value::<StateUpdate>(wire).unwrap(),
5445 without_details
5446 );
5447 }
5448 assert_eq!(
5449 serde_json::to_value(&without_details).unwrap(),
5450 json!({ "state": "idle", "stopReason": "error" })
5451 );
5452
5453 let parsed: StateUpdate = serde_json::from_value(json!({
5457 "state": "idle",
5458 "stopReason": "end_turn",
5459 "error": { "code": -32603, "message": "Internal error" }
5460 }))
5461 .unwrap();
5462 assert_eq!(
5463 parsed,
5464 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn))
5465 );
5466 }
5467
5468 #[test]
5469 fn flattened_stop_reason_keeps_open_enum_tolerance() {
5470 use crate::v2::OtherStopReason;
5471 use serde_json::json;
5472
5473 let meta: Meta = [("source".into(), json!("test"))].into_iter().collect();
5474
5475 for stop_reason in [None, Some(json!(null)), Some(json!(true)), Some(json!({}))] {
5478 let mut wire = json!({ "state": "idle", "_meta": { "source": "test" } });
5479 if let Some(stop_reason) = stop_reason {
5480 wire["stopReason"] = stop_reason;
5481 }
5482 let StateUpdate::Idle(idle) = serde_json::from_value(wire).unwrap() else {
5483 panic!("expected idle state update");
5484 };
5485 assert_eq!(idle.stop_reason, None);
5486 assert_eq!(idle.meta, Some(meta.clone()));
5487 }
5488
5489 let wire = json!({
5492 "state": "idle",
5493 "stopReason": "_paused",
5494 "resumeAfter": 30,
5495 "_meta": { "source": "test" }
5496 });
5497 let parsed: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
5498 let expected = StateUpdate::Idle(
5499 IdleStateUpdate::new()
5500 .stop_reason(StopReason::Other(OtherStopReason::new(
5501 "_paused",
5502 [("resumeAfter".into(), json!(30))].into_iter().collect(),
5503 )))
5504 .meta(meta),
5505 );
5506 assert_eq!(parsed, expected);
5507 assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
5508 }
5509
5510 #[test]
5511 fn known_stop_reasons_are_never_read_as_other() {
5512 use crate::v2::{ErrorStopReason, OtherStopReason};
5513 use serde_json::json;
5514
5515 let known = [
5516 StopReason::EndTurn,
5517 StopReason::MaxTokens,
5518 StopReason::MaxTurnRequests,
5519 StopReason::Refusal,
5520 StopReason::Cancelled,
5521 StopReason::Error(ErrorStopReason::new()),
5522 ];
5523 for stop_reason in &known {
5525 match stop_reason {
5526 StopReason::EndTurn
5527 | StopReason::MaxTokens
5528 | StopReason::MaxTurnRequests
5529 | StopReason::Refusal
5530 | StopReason::Cancelled
5531 | StopReason::Error(_)
5532 | StopReason::Other(_) => {}
5533 }
5534 }
5535
5536 for stop_reason in known {
5537 let update = StateUpdate::Idle(IdleStateUpdate::new().stop_reason(stop_reason));
5538 let wire = serde_json::to_value(&update).unwrap();
5539 assert_eq!(
5540 serde_json::from_value::<StateUpdate>(wire.clone()).unwrap(),
5541 update
5542 );
5543 assert!(
5544 serde_json::from_value::<OtherStopReason>(
5545 json!({ "stopReason": wire["stopReason"] })
5546 )
5547 .is_err(),
5548 "{} must not be accepted as a custom stop reason",
5549 wire["stopReason"]
5550 );
5551 }
5552 }
5553
5554 #[test]
5555 fn unknown_stop_reason_leaves_usage_on_the_idle_update() {
5556 use serde_json::json;
5557
5558 let usage = json!({ "totalTokens": 10, "inputTokens": 6, "outputTokens": 4 });
5559 let wire = json!({ "state": "idle", "stopReason": "_paused", "usage": usage });
5560 let StateUpdate::Idle(idle) = serde_json::from_value(wire.clone()).unwrap() else {
5561 panic!("expected idle state update");
5562 };
5563 let Some(StopReason::Other(other)) = &idle.stop_reason else {
5564 panic!("expected a custom stop reason");
5565 };
5566 assert!(other.fields.is_empty());
5567
5568 #[cfg(feature = "unstable_end_turn_token_usage")]
5569 {
5570 assert!(idle.usage.is_some());
5571 assert_eq!(serde_json::to_value(&idle).unwrap()["usage"], usage);
5572 }
5573 #[cfg(not(feature = "unstable_end_turn_token_usage"))]
5576 assert!(serde_json::to_value(&idle).unwrap().get("usage").is_none());
5577 }
5578
5579 #[test]
5580 fn test_state_update_serialization() {
5581 use serde_json::json;
5582
5583 assert_eq!(
5584 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
5585 RunningStateUpdate::new()
5586 )))
5587 .unwrap(),
5588 json!({
5589 "sessionUpdate": "state_update",
5590 "state": "running"
5591 })
5592 );
5593
5594 assert_eq!(
5595 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
5596 IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
5597 )))
5598 .unwrap(),
5599 json!({
5600 "sessionUpdate": "state_update",
5601 "state": "idle",
5602 "stopReason": "end_turn"
5603 })
5604 );
5605
5606 let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
5607 "sessionUpdate": "state_update",
5608 "state": "requires_action"
5609 }))
5610 .unwrap() else {
5611 panic!("expected state update");
5612 };
5613
5614 assert!(matches!(update, StateUpdate::RequiresAction(_)));
5615
5616 let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
5617 "sessionUpdate": "state_update",
5618 "state": "idle",
5619 "stopReason": null
5620 }))
5621 .unwrap() else {
5622 panic!("expected idle state update");
5623 };
5624
5625 assert_eq!(update.stop_reason, None);
5626
5627 let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
5628 serde_json::from_value(json!({
5629 "sessionUpdate": "state_update",
5630 "state": "_paused",
5631 "label": "Paused"
5632 }))
5633 .unwrap()
5634 else {
5635 panic!("expected unknown state update");
5636 };
5637
5638 assert_eq!(update.state, "_paused");
5639 assert_eq!(update.fields["label"], json!("Paused"));
5640 }
5641
5642 #[test]
5643 fn session_update_preserves_unknown_variant() {
5644 use serde_json::json;
5645
5646 let update: SessionUpdate = serde_json::from_value(json!({
5647 "sessionUpdate": "_status_badge",
5648 "label": "Indexing",
5649 "progress": 0.5
5650 }))
5651 .unwrap();
5652
5653 let SessionUpdate::Other(unknown) = update else {
5654 panic!("expected unknown session update");
5655 };
5656
5657 assert_eq!(unknown.session_update, "_status_badge");
5658 assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
5659 assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
5660
5661 assert_eq!(
5662 serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
5663 json!({
5664 "sessionUpdate": "_status_badge",
5665 "label": "Indexing",
5666 "progress": 0.5
5667 })
5668 );
5669 }
5670
5671 #[test]
5672 fn terminal_session_updates_use_known_discriminators() {
5673 use serde_json::json;
5674
5675 assert_eq!(
5676 serde_json::to_value(SessionUpdate::TerminalUpdate(
5677 TerminalUpdate::new("term_1").command("cargo test")
5678 ))
5679 .unwrap(),
5680 json!({
5681 "sessionUpdate": "terminal_update",
5682 "terminalId": "term_1",
5683 "command": "cargo test"
5684 })
5685 );
5686 assert_eq!(
5687 serde_json::to_value(SessionUpdate::TerminalOutputChunk(
5688 TerminalOutputChunk::new("term_1", "dGVzdAo=")
5689 ))
5690 .unwrap(),
5691 json!({
5692 "sessionUpdate": "terminal_output_chunk",
5693 "terminalId": "term_1",
5694 "data": "dGVzdAo="
5695 })
5696 );
5697 }
5698
5699 #[test]
5700 fn session_update_does_not_hide_malformed_known_terminal_variants() {
5701 use serde_json::json;
5702
5703 assert!(
5704 serde_json::from_value::<SessionUpdate>(json!({
5705 "sessionUpdate": "terminal_update"
5706 }))
5707 .is_err()
5708 );
5709 assert!(
5710 serde_json::from_value::<SessionUpdate>(json!({
5711 "sessionUpdate": "terminal_output_chunk",
5712 "terminalId": "term_1"
5713 }))
5714 .is_err()
5715 );
5716 }
5717
5718 #[test]
5719 fn test_plan_update_serialization() {
5720 use serde_json::json;
5721
5722 let plan_update =
5723 SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
5724 "plan-1",
5725 vec![crate::v2::PlanEntry::new(
5726 "Step 1",
5727 crate::v2::PlanEntryPriority::High,
5728 crate::v2::PlanEntryStatus::Pending,
5729 )],
5730 )));
5731
5732 assert_eq!(
5733 serde_json::to_value(plan_update).unwrap(),
5734 json!({
5735 "sessionUpdate": "plan_update",
5736 "plan": {
5737 "type": "items",
5738 "planId": "plan-1",
5739 "entries": [
5740 {
5741 "content": "Step 1",
5742 "priority": "high",
5743 "status": "pending"
5744 }
5745 ]
5746 }
5747 })
5748 );
5749 }
5750
5751 #[cfg(feature = "unstable_plan_operations")]
5752 #[test]
5753 fn test_plan_removed_serialization() {
5754 use serde_json::json;
5755
5756 assert_eq!(
5757 serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
5758 json!({
5759 "sessionUpdate": "plan_removed",
5760 "planId": "plan-1"
5761 })
5762 );
5763 }
5764
5765 #[test]
5766 fn available_command_uses_type_discriminator() {
5767 use serde_json::json;
5768
5769 let command = AvailableCommand::Command(
5770 Command::new("web", "Search the web")
5771 .input(AvailableCommandInput::Text(TextCommandInput::new("Query"))),
5772 );
5773
5774 let json = serde_json::to_value(&command).unwrap();
5775 assert_eq!(
5776 json,
5777 json!({
5778 "type": "command",
5779 "name": "web",
5780 "description": "Search the web",
5781 "input": { "type": "text", "hint": "Query" }
5782 })
5783 );
5784
5785 let roundtripped: AvailableCommand = serde_json::from_value(json).unwrap();
5786 assert_eq!(roundtripped, command);
5787 assert_eq!(roundtripped.name(), "web");
5788 assert_eq!(roundtripped.description(), "Search the web");
5789 }
5790
5791 #[test]
5792 fn available_command_preserves_unknown_typed_variant() {
5793 use serde_json::json;
5794
5795 let payload = json!({
5796 "type": "_skill",
5797 "name": "pdf-processing",
5798 "description": "Extract text from PDFs",
5799 "uri": "skill://pdf-processing/SKILL.md",
5800 "_meta": { "example.com/source": "mcp" }
5801 });
5802 let command: AvailableCommand = serde_json::from_value(payload.clone()).unwrap();
5803
5804 assert_eq!(command.name(), "pdf-processing");
5805 assert_eq!(command.description(), "Extract text from PDFs");
5806 assert!(command.meta().is_some());
5807
5808 let AvailableCommand::Other(unknown) = &command else {
5809 panic!("expected unknown command");
5810 };
5811 assert_eq!(unknown.type_, "_skill");
5812 assert_eq!(
5813 unknown.fields.get("uri"),
5814 Some(&json!("skill://pdf-processing/SKILL.md"))
5815 );
5816 assert_eq!(serde_json::to_value(&command).unwrap(), payload);
5817 }
5818
5819 #[test]
5820 fn available_command_unknown_tolerates_invalid_meta() {
5821 use serde_json::json;
5822
5823 let command: AvailableCommand = serde_json::from_value(json!({
5824 "type": "_skill",
5825 "name": "pdf-processing",
5826 "description": "Extract text from PDFs",
5827 "_meta": "oops"
5828 }))
5829 .unwrap();
5830
5831 assert!(matches!(command, AvailableCommand::Other(_)));
5832 assert!(command.meta().is_none());
5833 }
5834
5835 #[test]
5836 fn available_command_unknown_does_not_hide_malformed_command_variant() {
5837 use serde_json::json;
5838
5839 assert!(
5841 serde_json::from_value::<AvailableCommand>(json!({
5842 "name": "test",
5843 "description": "Run project tests"
5844 }))
5845 .is_err()
5846 );
5847 assert!(
5848 serde_json::from_value::<AvailableCommand>(json!({
5849 "type": "command",
5850 "name": "test"
5851 }))
5852 .is_err()
5853 );
5854 assert!(
5855 serde_json::from_value::<OtherAvailableCommand>(json!({
5856 "type": "command",
5857 "name": "test",
5858 "description": "Run project tests"
5859 }))
5860 .is_err()
5861 );
5862 assert!(
5863 serde_json::from_value::<AvailableCommand>(json!({
5864 "type": "_skill",
5865 "name": "pdf-processing"
5866 }))
5867 .is_err()
5868 );
5869 }
5870
5871 #[test]
5872 fn available_command_input_preserves_unknown_typed_variant() {
5873 use serde_json::json;
5874
5875 let input: AvailableCommandInput = serde_json::from_value(json!({
5876 "type": "_choices",
5877 "hint": "Pick one",
5878 "options": ["fast", "careful"]
5879 }))
5880 .unwrap();
5881
5882 let AvailableCommandInput::Other(unknown) = input else {
5883 panic!("expected unknown command input");
5884 };
5885
5886 assert_eq!(unknown.type_, "_choices");
5887 assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
5888 assert_eq!(
5889 unknown.fields.get("options"),
5890 Some(&json!(["fast", "careful"]))
5891 );
5892 assert_eq!(
5893 serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
5894 json!({
5895 "type": "_choices",
5896 "hint": "Pick one",
5897 "options": ["fast", "careful"]
5898 })
5899 );
5900 }
5901
5902 #[test]
5903 fn available_command_input_text_uses_type_discriminator() {
5904 use serde_json::json;
5905
5906 let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
5907
5908 let json = serde_json::to_value(&input).unwrap();
5909 assert_eq!(
5910 json,
5911 json!({
5912 "type": "text",
5913 "hint": "Describe changes"
5914 })
5915 );
5916
5917 let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
5918 assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
5919 }
5920
5921 #[test]
5922 fn request_permission_subject_tool_call_uses_type_discriminator() {
5923 use serde_json::json;
5924
5925 let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
5926
5927 let json = serde_json::to_value(&subject).unwrap();
5928 assert_eq!(
5929 json,
5930 json!({
5931 "type": "tool_call",
5932 "toolCall": {
5933 "toolCallId": "call_001"
5934 }
5935 })
5936 );
5937
5938 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5939 assert!(matches!(
5940 roundtripped,
5941 RequestPermissionSubject::ToolCall(_)
5942 ));
5943 }
5944
5945 #[test]
5946 fn request_permission_subject_command_uses_type_discriminator() {
5947 use serde_json::json;
5948
5949 let mut meta = Meta::new();
5950 meta.insert("source".to_string(), json!("shell"));
5951 let subject = RequestPermissionSubject::from(
5952 CommandPermissionSubject::new("cargo test", "/workspace/project")
5953 .tool_call_id("call_001")
5954 .terminal_id("term_1")
5955 .meta(meta),
5956 );
5957
5958 let json = serde_json::to_value(&subject).unwrap();
5959 assert_eq!(
5960 json,
5961 json!({
5962 "type": "command",
5963 "command": "cargo test",
5964 "cwd": "/workspace/project",
5965 "toolCallId": "call_001",
5966 "terminalId": "term_1",
5967 "_meta": {
5968 "source": "shell"
5969 }
5970 })
5971 );
5972
5973 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5974 assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
5975 }
5976
5977 #[test]
5978 fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
5979 use serde_json::json;
5980
5981 let subject: RequestPermissionSubject = serde_json::from_value(json!({
5982 "type": "command",
5983 "command": "cargo test",
5984 "cwd": "/workspace/project",
5985 "toolCallId": null,
5986 "terminalId": null,
5987 "_meta": null
5988 }))
5989 .unwrap();
5990
5991 let RequestPermissionSubject::Command(subject) = subject else {
5992 panic!("expected command permission subject");
5993 };
5994 assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
5995 assert_eq!(subject.tool_call_id, None);
5996 assert_eq!(subject.terminal_id, None);
5997 assert_eq!(subject.meta, None);
5998 assert_eq!(
5999 serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
6000 json!({
6001 "type": "command",
6002 "command": "cargo test",
6003 "cwd": "/workspace/project"
6004 })
6005 );
6006 }
6007
6008 #[test]
6009 fn request_permission_subject_preserves_unknown_variant() {
6010 use serde_json::json;
6011
6012 let subject: RequestPermissionSubject = serde_json::from_value(json!({
6013 "type": "_review",
6014 "reason": "needs-review",
6015 "retryAfterSeconds": 30
6016 }))
6017 .unwrap();
6018
6019 let RequestPermissionSubject::Other(unknown) = subject else {
6020 panic!("expected unknown permission subject");
6021 };
6022
6023 assert_eq!(unknown.type_, "_review");
6024 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
6025 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
6026 assert_eq!(
6027 serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
6028 json!({
6029 "type": "_review",
6030 "reason": "needs-review",
6031 "retryAfterSeconds": 30
6032 })
6033 );
6034 }
6035
6036 #[test]
6037 fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
6038 use serde_json::json;
6039
6040 assert!(
6041 serde_json::from_value::<RequestPermissionSubject>(json!({
6042 "type": "tool_call"
6043 }))
6044 .is_err()
6045 );
6046 assert!(
6047 serde_json::from_value::<RequestPermissionSubject>(json!({
6048 "type": 1
6049 }))
6050 .is_err()
6051 );
6052 assert!(
6053 serde_json::from_value::<RequestPermissionSubject>(json!({
6054 "type": "command",
6055 "cwd": "/workspace/project"
6056 }))
6057 .is_err()
6058 );
6059 assert!(
6060 serde_json::from_value::<RequestPermissionSubject>(json!({
6061 "type": "command",
6062 "command": "cargo test"
6063 }))
6064 .is_err()
6065 );
6066 assert!(
6067 serde_json::from_value::<RequestPermissionSubject>(json!({
6068 "type": "command",
6069 "command": "cargo test",
6070 "cwd": null
6071 }))
6072 .is_err()
6073 );
6074 }
6075
6076 #[test]
6077 fn request_permission_title_and_description_are_separate_from_tool_call_content() {
6078 use serde_json::json;
6079
6080 let request =
6081 RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
6082 .description("Allow this tool to edit src/main.rs?")
6083 .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
6084 "call_001",
6085 )));
6086
6087 assert_eq!(
6088 serde_json::to_value(request).unwrap(),
6089 json!({
6090 "sessionId": "sess_abc123def456",
6091 "title": "Approve file edit?",
6092 "description": "Allow this tool to edit src/main.rs?",
6093 "subject": {
6094 "type": "tool_call",
6095 "toolCall": {
6096 "toolCallId": "call_001"
6097 }
6098 },
6099 "options": []
6100 })
6101 );
6102 }
6103
6104 #[test]
6105 fn request_permission_requires_title_and_allows_missing_subject() {
6106 use serde_json::json;
6107
6108 let request = RequestPermissionRequest::new(
6109 "sess_abc123def456",
6110 "Approve elevated permissions?",
6111 Vec::new(),
6112 );
6113
6114 assert_eq!(
6115 serde_json::to_value(request).unwrap(),
6116 json!({
6117 "sessionId": "sess_abc123def456",
6118 "title": "Approve elevated permissions?",
6119 "options": []
6120 })
6121 );
6122
6123 let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
6124 "sessionId": "sess_abc123def456",
6125 "title": "Approve elevated permissions?",
6126 "options": []
6127 }))
6128 .unwrap();
6129 assert!(missing_subject.subject.is_none());
6130
6131 let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
6132 "sessionId": "sess_abc123def456",
6133 "title": "Approve elevated permissions?",
6134 "subject": null,
6135 "options": []
6136 }))
6137 .unwrap();
6138 assert!(null_subject.subject.is_none());
6139
6140 assert!(
6141 serde_json::from_value::<RequestPermissionRequest>(json!({
6142 "sessionId": "sess_abc123def456",
6143 "options": []
6144 }))
6145 .is_err()
6146 );
6147 }
6148
6149 #[test]
6150 fn request_permission_outcome_preserves_unknown_variant() {
6151 use serde_json::json;
6152
6153 let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
6154 "outcome": "_defer",
6155 "reason": "needs-review",
6156 "retryAfterSeconds": 30
6157 }))
6158 .unwrap();
6159
6160 let RequestPermissionOutcome::Other(unknown) = outcome else {
6161 panic!("expected unknown permission outcome");
6162 };
6163
6164 assert_eq!(unknown.outcome, "_defer");
6165 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
6166 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
6167 assert_eq!(
6168 serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
6169 json!({
6170 "outcome": "_defer",
6171 "reason": "needs-review",
6172 "retryAfterSeconds": 30
6173 })
6174 );
6175 }
6176
6177 #[test]
6178 fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
6179 use serde_json::json;
6180
6181 assert!(
6182 serde_json::from_value::<RequestPermissionOutcome>(json!({
6183 "outcome": "selected"
6184 }))
6185 .is_err()
6186 );
6187 assert!(
6188 serde_json::from_value::<RequestPermissionOutcome>(json!({
6189 "outcome": 1
6190 }))
6191 .is_err()
6192 );
6193 }
6194
6195 #[test]
6196 fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
6197 use serde_json::json;
6198
6199 assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
6200 assert!(
6201 serde_json::from_value::<AvailableCommandInput>(json!({
6202 "hint": "Pick one"
6203 }))
6204 .is_err()
6205 );
6206 assert!(
6207 serde_json::from_value::<AvailableCommandInput>(json!({
6208 "type": 1,
6209 "hint": "Pick one"
6210 }))
6211 .is_err()
6212 );
6213 assert!(
6214 serde_json::from_value::<OtherAvailableCommandInput>(json!({
6215 "type": "text",
6216 "hint": "Pick one"
6217 }))
6218 .is_err()
6219 );
6220 }
6221
6222 #[cfg(feature = "unstable_mcp_over_acp")]
6223 #[test]
6224 fn test_agent_mcp_request_method_names() {
6225 use serde_json::json;
6226
6227 let params: serde_json::Map<String, serde_json::Value> =
6228 [("cursor".to_string(), json!("abc"))].into_iter().collect();
6229
6230 assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
6231 assert_eq!(
6232 AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
6233 "server-1",
6234 "req-1",
6235 "tools/list"
6236 )))
6237 .method(),
6238 "mcp/message"
6239 );
6240 assert_eq!(
6241 serde_json::to_value(
6242 MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
6243 )
6244 .unwrap(),
6245 json!({
6246 "serverId": "server-1",
6247 "requestId": "req-1",
6248 "method": "tools/list",
6249 "params": { "cursor": "abc" }
6250 })
6251 );
6252
6253 let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
6254 "serverId": "server-1",
6255 "requestId": "req-1",
6256 "method": "tools/list",
6257 "params": null,
6258 "_meta": null
6259 }))
6260 .unwrap();
6261 assert_eq!(request_with_null_params.params, None);
6262 assert_eq!(request_with_null_params.meta, None);
6263 for key in ["serverId", "requestId", "method"] {
6264 let mut value =
6265 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
6266 value.as_object_mut().unwrap().remove(key);
6267 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
6268 }
6269 for key in ["serverId", "requestId", "method"] {
6270 let mut value =
6271 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
6272 value[key] = serde_json::Value::Null;
6273 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
6274 }
6275 }
6276
6277 #[test]
6278 fn test_auth_capabilities_serialize_terminal_support_as_object() {
6279 use serde_json::json;
6280
6281 let capabilities = ClientAuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
6282
6283 assert_eq!(
6284 serde_json::to_value(&capabilities).unwrap(),
6285 json!({
6286 "terminal": {}
6287 })
6288 );
6289
6290 let deserialized: ClientAuthCapabilities = serde_json::from_value(json!({
6291 "terminal": false
6292 }))
6293 .unwrap();
6294 assert!(deserialized.terminal.is_none());
6295 }
6296
6297 #[test]
6298 fn request_permission_request_rejects_malformed_options() {
6299 use serde_json::json;
6300
6301 assert!(
6302 serde_json::from_value::<RequestPermissionRequest>(json!({
6303 "sessionId": "sess-1",
6304 "title": "Run tool?",
6305 "options": "not-an-array"
6306 }))
6307 .is_err()
6308 );
6309 assert!(
6310 serde_json::from_value::<RequestPermissionRequest>(json!({
6311 "sessionId": "sess-1",
6312 "title": "Run tool?",
6313 "options": [{"optionId": "allow"}]
6314 }))
6315 .is_err()
6316 );
6317 }
6318
6319 #[cfg(feature = "unstable_plan_operations")]
6320 #[test]
6321 fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
6322 use serde_json::json;
6323
6324 assert!(
6325 serde_json::from_value::<SessionUpdate>(json!({
6326 "sessionUpdate": "plan_removed"
6327 }))
6328 .is_err()
6329 );
6330 }
6331}