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>),
3986 CreateElicitationRequest(Box<CreateElicitationRequest>),
3990 #[cfg(feature = "unstable_mcp_over_acp")]
3996 MessageMcpRequest(Box<MessageMcpRequest>),
3997 ExtMethodRequest(Box<ExtRequest>),
4005}
4006
4007impl AgentRequest {
4008 #[must_use]
4010 pub fn method(&self) -> &str {
4011 match self {
4012 Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission,
4013 Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create,
4014 #[cfg(feature = "unstable_mcp_over_acp")]
4015 Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message,
4016 Self::ExtMethodRequest(ext_request) => &ext_request.method,
4017 }
4018 }
4019}
4020
4021#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
4028#[derive(Clone, Debug, Serialize, Deserialize)]
4029#[serde(untagged)]
4030#[cfg_attr(feature = "schemars", schemars(inline))]
4031#[non_exhaustive]
4032pub enum ClientResponse {
4033 RequestPermissionResponse(Box<RequestPermissionResponse>),
4035 CreateElicitationResponse(Box<CreateElicitationResponse>),
4037 #[cfg(feature = "unstable_mcp_over_acp")]
4039 MessageMcpResponse(Box<MessageMcpResponse>),
4040 ExtMethodResponse(Box<ExtResponse>),
4042}
4043
4044#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
4051#[derive(Clone, Debug, Serialize, Deserialize)]
4052#[serde(untagged)]
4053#[cfg_attr(feature = "schemars", schemars(inline))]
4054#[non_exhaustive]
4055pub enum AgentNotification {
4056 UpdateSessionNotification(Box<UpdateSessionNotification>),
4069 CompleteElicitationNotification(Box<CompleteElicitationNotification>),
4073 #[cfg(feature = "unstable_auth_status")]
4081 AuthStatusUpdateNotification(Box<AuthStatusUpdateNotification>),
4082 ExtNotification(Box<ExtNotification>),
4090}
4091
4092impl AgentNotification {
4093 #[must_use]
4095 pub fn method(&self) -> &str {
4096 match self {
4097 Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update,
4098 Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete,
4099 #[cfg(feature = "unstable_auth_status")]
4100 Self::AuthStatusUpdateNotification(_) => CLIENT_METHOD_NAMES.auth_status_update,
4101 Self::ExtNotification(ext_notification) => &ext_notification.method,
4102 }
4103 }
4104}
4105
4106#[cfg(test)]
4107mod tests {
4108
4109 #[test]
4110 fn background_list_round_trips_on_state_updates() {
4111 use serde_json::json;
4112 let idle = super::StateUpdate::Idle(super::IdleStateUpdate::new().background(vec![
4113 super::BackgroundWork::Terminal(super::BackgroundTerminal::new("term_003")),
4114 super::BackgroundWork::ToolCall(super::BackgroundToolCall::new("call_012")),
4115 ]));
4116 let wire = json!({
4117 "state": "idle",
4118 "background": [
4119 { "type": "terminal", "terminalId": "term_003" },
4120 { "type": "tool_call", "toolCallId": "call_012" }
4121 ]
4122 });
4123 assert_eq!(serde_json::to_value(&idle).unwrap(), wire);
4124 assert_eq!(
4125 serde_json::from_value::<super::StateUpdate>(wire).unwrap(),
4126 idle
4127 );
4128
4129 assert_eq!(idle.background().map(<[_]>::len), Some(2));
4130
4131 let empty = super::RunningStateUpdate::new().background(vec![]);
4132 assert_eq!(
4133 serde_json::to_value(&empty).unwrap(),
4134 json!({ "background": [] })
4135 );
4136 assert_eq!(
4137 super::StateUpdate::Running(empty).background(),
4138 Some(&[][..])
4139 );
4140 assert_eq!(
4141 super::StateUpdate::RequiresAction(super::RequiresActionStateUpdate::new())
4142 .background(),
4143 None
4144 );
4145 let custom: super::StateUpdate = serde_json::from_value(json!({
4146 "state": "_waiting",
4147 "background": [{ "type": "terminal", "terminalId": "term_003" }]
4148 }))
4149 .unwrap();
4150 assert_eq!(custom.background(), None);
4151 }
4152
4153 #[test]
4154 fn background_list_keeps_unknown_items_and_drops_malformed_lists() {
4155 use serde_json::json;
4156 let parsed: super::RunningStateUpdate = serde_json::from_value(json!({
4157 "background": [{ "type": "_cron", "schedule": "* * * * *" }]
4158 }))
4159 .unwrap();
4160 let Some([super::BackgroundWork::Other(other)]) = parsed.background.as_deref() else {
4161 panic!("expected one unknown item: {parsed:?}");
4162 };
4163 assert_eq!(other.kind, "_cron");
4164 assert_eq!(other.fields["schedule"], json!("* * * * *"));
4165
4166 for background in [
4167 json!(null),
4168 json!("oops"),
4169 json!([{ "type": "terminal" }]),
4170 json!([{ "type": "tool_call", "toolCallId": "call_1" }, { "type": "terminal", "terminalId": 7 }]),
4171 ] {
4172 let parsed: super::IdleStateUpdate =
4173 serde_json::from_value(json!({ "background": background })).unwrap();
4174 assert_eq!(parsed.background, None, "{background}");
4175 }
4176 }
4177
4178 #[test]
4179 fn replacement_lists_reject_non_arrays_instead_of_clearing() {
4180 use serde_json::json;
4181 for (update, key) in [
4182 ("config_option_update", "configOptions"),
4183 ("available_commands_update", "availableCommands"),
4184 ] {
4185 let empty = json!({ "sessionUpdate": update, key: [] });
4186 serde_json::from_value::<super::SessionUpdate>(empty).unwrap();
4187 for bad in [json!("oops"), json!(null), json!({})] {
4188 let wire = json!({ "sessionUpdate": update, key: bad });
4189 assert!(
4190 serde_json::from_value::<super::SessionUpdate>(wire).is_err(),
4191 "{update} {bad}"
4192 );
4193 }
4194 }
4195 let plan = |entries: serde_json::Value| {
4196 json!({ "sessionUpdate": "plan_update",
4197 "plan": { "type": "items", "planId": "p", "entries": entries } })
4198 };
4199 serde_json::from_value::<super::SessionUpdate>(plan(json!([]))).unwrap();
4200 assert!(serde_json::from_value::<super::SessionUpdate>(plan(json!("oops"))).is_err());
4201 let skipped =
4203 plan(json!([42, { "content": "a", "priority": "high", "status": "pending" }]));
4204 let super::SessionUpdate::PlanUpdate(update) =
4205 serde_json::from_value::<super::SessionUpdate>(skipped).unwrap()
4206 else {
4207 panic!("expected plan_update");
4208 };
4209 let crate::v2::PlanUpdateContent::Items(items) = update.plan else {
4210 panic!("expected items");
4211 };
4212 assert_eq!(items.entries.len(), 1);
4213 }
4214
4215 #[test]
4216 fn other_available_command_preserves_meta_verbatim() {
4217 for meta in [serde_json::json!({ "k": 1 }), serde_json::json!("bad")] {
4218 let wire = serde_json::json!({ "type": "_custom", "name": "c",
4219 "description": "d", "_meta": meta, "extra": true });
4220 let command: super::AvailableCommand = serde_json::from_value(wire.clone()).unwrap();
4221 assert_eq!(command.meta(), meta.as_object());
4222 assert_eq!(serde_json::to_value(&command).unwrap(), wire);
4223 }
4224 }
4225
4226 #[test]
4227 fn cancelled_permission_outcome_wire_format() {
4228 let outcome: RequestPermissionOutcome =
4229 serde_json::from_value(serde_json::json!({ "outcome": "cancelled" })).unwrap();
4230 assert_eq!(outcome, CancelledPermissionOutcome::new().into());
4231 assert_eq!(
4232 serde_json::to_value(&outcome).unwrap(),
4233 serde_json::json!({ "outcome": "cancelled" })
4234 );
4235
4236 let with_meta: RequestPermissionOutcome = CancelledPermissionOutcome::new()
4237 .meta(serde_json::Map::from_iter([(
4238 "k".to_string(),
4239 serde_json::json!(1),
4240 )]))
4241 .into();
4242 let json = serde_json::to_value(&with_meta).unwrap();
4243 assert_eq!(
4244 json,
4245 serde_json::json!({ "outcome": "cancelled", "_meta": { "k": 1 } })
4246 );
4247 assert_eq!(
4248 serde_json::from_value::<RequestPermissionOutcome>(json).unwrap(),
4249 with_meta
4250 );
4251 }
4252
4253 use super::*;
4254
4255 #[cfg(feature = "unstable_subagents")]
4256 #[test]
4257 fn subagent_update_serializes_as_upsert() {
4258 use serde_json::json;
4259
4260 let announced =
4261 SessionUpdate::SubagentUpdate(SubagentUpdate::new("sess_child_1").capabilities(
4262 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new()),
4263 ));
4264 let wire = json!({
4265 "sessionUpdate": "subagent_update",
4266 "sessionId": "sess_child_1",
4267 "capabilities": { "cancel": {} }
4268 });
4269 assert_eq!(serde_json::to_value(&announced).unwrap(), wire);
4270 assert_eq!(
4271 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4272 announced
4273 );
4274
4275 let minimal = SubagentUpdate::new("sess_child_1");
4276 assert_eq!(
4277 serde_json::to_value(&minimal).unwrap(),
4278 json!({
4279 "sessionId": "sess_child_1"
4280 })
4281 );
4282 assert!(minimal.capabilities.is_undefined());
4283 assert!(minimal.state.is_undefined());
4284 assert!(minimal.meta.is_undefined());
4285
4286 let cleared_wire = json!({
4288 "sessionId": "sess_child_1",
4289 "capabilities": null,
4290 "state": null,
4291 "_meta": null
4292 });
4293 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4294 assert!(cleared.capabilities.is_null());
4295 assert!(cleared.state.is_null());
4296 assert!(cleared.meta.is_null());
4297 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4298
4299 let revoked: SubagentUpdate = serde_json::from_value(json!({
4300 "sessionId": "sess_child_1",
4301 "capabilities": {}
4302 }))
4303 .unwrap();
4304 assert_eq!(
4305 revoked.capabilities,
4306 MaybeUndefined::Value(SubagentSessionCapabilities::new())
4307 );
4308 assert!(
4309 matches!(revoked.capabilities, MaybeUndefined::Value(ref child) if child.cancel.is_none())
4310 );
4311
4312 assert!(
4313 serde_json::from_value::<SessionUpdate>(json!({
4314 "sessionUpdate": "subagent_update"
4315 }))
4316 .is_err()
4317 );
4318 }
4319
4320 #[cfg(feature = "unstable_subagents")]
4321 #[test]
4322 fn subagent_cancel_capability_is_optional_object() {
4323 use serde_json::json;
4324
4325 for wire in [
4326 json!({}),
4327 json!({"cancel": null}),
4328 json!({"cancel": true}),
4329 json!({"cancel": false}),
4330 ] {
4331 let child: SubagentSessionCapabilities = serde_json::from_value(wire).unwrap();
4332 assert!(child.cancel.is_none());
4333 assert_eq!(serde_json::to_value(child).unwrap(), json!({}));
4334 }
4335 let enabled = SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new());
4336 assert_eq!(
4337 serde_json::to_value(&enabled).unwrap(),
4338 json!({"cancel": {}})
4339 );
4340 assert_eq!(
4341 serde_json::from_value::<SubagentSessionCapabilities>(json!({"cancel": {}})).unwrap(),
4342 enabled
4343 );
4344 let meta: Meta = [("source".into(), json!("worker"))].into_iter().collect();
4345 let with_meta =
4346 SubagentSessionCapabilities::new().cancel(SessionCancelCapabilities::new().meta(meta));
4347 let wire = json!({"cancel": {"_meta": {"source": "worker"}}});
4348 assert_eq!(serde_json::to_value(&with_meta).unwrap(), wire);
4349 assert_eq!(
4350 serde_json::from_value::<SubagentSessionCapabilities>(wire).unwrap(),
4351 with_meta
4352 );
4353 assert!(enabled.cancel.is_some());
4354 assert!(enabled.cancel(None).cancel.is_none());
4355 let removed = SubagentUpdate::new("child").capabilities(SubagentSessionCapabilities::new());
4356 assert_eq!(
4357 serde_json::to_value(removed).unwrap(),
4358 json!({"sessionId": "child", "capabilities": {}})
4359 );
4360 }
4361
4362 #[cfg(feature = "unstable_subagents")]
4363 #[test]
4364 fn subagent_display_metadata_uses_patch_semantics() {
4365 use serde_json::json;
4366
4367 let update = SubagentUpdate::new("child")
4368 .title("Test investigator".to_string())
4369 .description("Investigates platform-specific test failures.".to_string());
4370 let wire = json!({
4371 "sessionId": "child",
4372 "title": "Test investigator",
4373 "description": "Investigates platform-specific test failures."
4374 });
4375 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4376 assert_eq!(
4377 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4378 update
4379 );
4380
4381 let minimal = SubagentUpdate::new("child");
4382 assert!(minimal.title.is_undefined());
4383 assert!(minimal.description.is_undefined());
4384 assert_eq!(
4385 serde_json::to_value(&minimal).unwrap(),
4386 json!({"sessionId": "child"})
4387 );
4388 let cleared_wire = json!({
4389 "sessionId": "child", "title": null, "description": null
4390 });
4391 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4392 assert!(cleared.title.is_null());
4393 assert!(cleared.description.is_null());
4394 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4395
4396 let title_only: SubagentUpdate = serde_json::from_value(json!({
4397 "sessionId": "child", "title": "Updated title"
4398 }))
4399 .unwrap();
4400 assert_eq!(
4401 title_only.title,
4402 MaybeUndefined::Value("Updated title".to_string())
4403 );
4404 assert!(title_only.description.is_undefined());
4405 let malformed: SubagentUpdate = serde_json::from_value(json!({
4406 "sessionId": "child", "title": false, "description": false
4407 }))
4408 .unwrap();
4409 assert_eq!(malformed, minimal);
4410 }
4411
4412 #[cfg(feature = "unstable_subagents")]
4413 #[test]
4414 fn subagent_directories_use_strict_patch_semantics() {
4415 use serde_json::json;
4416
4417 let update = SubagentUpdate::new("child")
4418 .cwd("/home/user/project/.worktrees/investigator")
4419 .additional_directories(vec![]);
4420 let wire = json!({
4421 "sessionId": "child",
4422 "cwd": "/home/user/project/.worktrees/investigator",
4423 "additionalDirectories": []
4424 });
4425 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4426 assert_eq!(
4427 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4428 update
4429 );
4430
4431 let inherited: SubagentUpdate =
4432 serde_json::from_value(json!({"sessionId": "child"})).unwrap();
4433 assert!(inherited.cwd.is_undefined());
4434 assert!(inherited.additional_directories.is_undefined());
4435
4436 let cleared_wire = json!({
4437 "sessionId": "child", "cwd": null, "additionalDirectories": null
4438 });
4439 let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4440 assert!(cleared.cwd.is_null());
4441 assert!(cleared.additional_directories.is_null());
4442 assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4443
4444 for malformed in [
4445 json!({"sessionId": "child", "cwd": false}),
4446 json!({"sessionId": "child", "additionalDirectories": "/tmp/wt"}),
4447 json!({"sessionId": "child", "additionalDirectories": ["/tmp/wt", false]}),
4448 ] {
4449 assert!(
4450 serde_json::from_value::<SubagentUpdate>(malformed.clone()).is_err(),
4451 "{malformed}"
4452 );
4453 }
4454 }
4455
4456 #[cfg(feature = "unstable_subagents")]
4457 #[test]
4458 fn subagent_notification_keeps_parent_and_child_ids_nested() {
4459 use serde_json::json;
4460
4461 let notification = UpdateSessionNotification::new(
4462 "parent",
4463 SessionUpdate::SubagentUpdate(SubagentUpdate::new("child")),
4464 );
4465 let wire = json!({
4466 "sessionId": "parent",
4467 "update": {
4468 "sessionUpdate": "subagent_update",
4469 "sessionId": "child"
4470 }
4471 });
4472 assert_eq!(serde_json::to_value(¬ification).unwrap(), wire);
4473 assert_eq!(
4474 serde_json::from_value::<UpdateSessionNotification>(wire).unwrap(),
4475 notification
4476 );
4477 for malformed in [
4478 json!({"sessionId": "parent", "update": {"sessionUpdate": "subagent_update"}}),
4479 json!({"update": {"sessionUpdate": "subagent_update", "sessionId": "child"}}),
4480 ] {
4481 assert!(serde_json::from_value::<UpdateSessionNotification>(malformed).is_err());
4482 }
4483 }
4484
4485 #[cfg(feature = "unstable_subagents")]
4486 #[test]
4487 fn subagent_mirrors_normal_child_state_notifications() {
4488 use serde_json::json;
4489
4490 let association = SubagentUpdate::new("sess_child");
4491 for state in [
4492 StateUpdate::Running(RunningStateUpdate::new()),
4493 StateUpdate::RequiresAction(RequiresActionStateUpdate::new()),
4494 StateUpdate::Unknown(UnknownStateUpdate::new()),
4495 StateUpdate::Running(RunningStateUpdate::new()),
4496 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn)),
4497 StateUpdate::Running(RunningStateUpdate::new()),
4498 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::Cancelled)),
4499 ] {
4500 let child_notification = UpdateSessionNotification::new(
4501 association.session_id.clone(),
4502 SessionUpdate::StateUpdate(state.clone()),
4503 );
4504 let parent_notification = UpdateSessionNotification::new(
4505 "sess_parent",
4506 SessionUpdate::SubagentUpdate(association.clone().state(state)),
4507 );
4508 let child_wire = serde_json::to_value(&child_notification).unwrap();
4509 let parent_wire = serde_json::to_value(&parent_notification).unwrap();
4510 assert_eq!(child_wire["sessionId"], json!("sess_child"));
4511 assert_eq!(child_wire["update"]["sessionUpdate"], json!("state_update"));
4512 assert_eq!(parent_wire["sessionId"], json!("sess_parent"));
4513 assert_eq!(parent_wire["update"]["sessionId"], json!("sess_child"));
4514 let mut child_snapshot = child_wire["update"].clone();
4515 child_snapshot
4516 .as_object_mut()
4517 .unwrap()
4518 .remove("sessionUpdate");
4519 assert_eq!(parent_wire["update"]["state"], child_snapshot);
4520 assert_eq!(
4521 serde_json::from_value::<UpdateSessionNotification>(child_wire).unwrap(),
4522 child_notification
4523 );
4524 assert_eq!(
4525 serde_json::from_value::<UpdateSessionNotification>(parent_wire).unwrap(),
4526 parent_notification
4527 );
4528 }
4529 }
4530
4531 #[cfg(feature = "unstable_subagents")]
4532 #[test]
4533 fn subagent_state_uses_whole_snapshot_patch_semantics() {
4534 use serde_json::json;
4535
4536 let minimal = SubagentUpdate::new("child");
4537 assert!(minimal.state.is_undefined());
4538 assert_eq!(
4539 serde_json::to_value(&minimal).unwrap(),
4540 json!({"sessionId": "child"})
4541 );
4542
4543 let cleared = minimal.clone().state(None::<StateUpdate>);
4544 assert!(cleared.state.is_null());
4545 assert_eq!(
4546 serde_json::to_value(&cleared).unwrap(),
4547 json!({"sessionId": "child", "state": null})
4548 );
4549 assert_eq!(
4550 serde_json::from_value::<SubagentUpdate>(json!({"sessionId": "child", "state": null}))
4551 .unwrap(),
4552 cleared
4553 );
4554
4555 for snapshot in [
4556 json!({"state": "running", "_meta": {"source": "worker"}}),
4557 json!({"state": "idle", "stopReason": "end_turn"}),
4558 json!({"state": "requires_action"}),
4559 json!({"state": "unknown"}),
4560 json!({"state": "_custom", "detail": {"phase": 2}}),
4561 ] {
4562 let state: StateUpdate = serde_json::from_value(snapshot.clone()).unwrap();
4563 let update = minimal.clone().state(state.clone());
4564 let wire = json!({"sessionId": "child", "state": snapshot});
4565 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4566 assert_eq!(
4567 serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4568 update
4569 );
4570 assert_eq!(update.state, MaybeUndefined::Value(state));
4571 }
4572
4573 let idle = SubagentUpdate::new("child").state(StateUpdate::Idle(
4576 IdleStateUpdate::new().stop_reason(StopReason::Cancelled),
4577 ));
4578 let running = idle.state(StateUpdate::Running(RunningStateUpdate::new()));
4579 assert_eq!(
4580 serde_json::to_value(running).unwrap(),
4581 json!({"sessionId": "child", "state": {"state": "running"}})
4582 );
4583 for invalid in [json!(false), json!(42), json!({}), json!({"state": 7})] {
4584 let parsed: SubagentUpdate =
4585 serde_json::from_value(json!({"sessionId": "child", "state": invalid})).unwrap();
4586 assert_eq!(parsed, minimal);
4587 }
4588 }
4589
4590 #[cfg(feature = "unstable_subagents")]
4591 #[test]
4592 fn unknown_activity_is_a_known_state_update() {
4593 use serde_json::json;
4594
4595 let update = SessionUpdate::StateUpdate(StateUpdate::Unknown(
4596 UnknownStateUpdate::new().meta(
4597 [("source".into(), json!("worker"))]
4598 .into_iter()
4599 .collect::<Meta>(),
4600 ),
4601 ));
4602 let wire = json!({
4603 "sessionUpdate": "state_update",
4604 "state": "unknown",
4605 "_meta": { "source": "worker" }
4606 });
4607 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4608 assert_eq!(
4609 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
4610 update
4611 );
4612
4613 for meta in [json!(null), json!(false)] {
4614 let state: StateUpdate = serde_json::from_value(json!({
4615 "state": "unknown",
4616 "_meta": meta
4617 }))
4618 .unwrap();
4619 assert_eq!(state, StateUpdate::Unknown(UnknownStateUpdate::new()));
4620 assert_eq!(
4621 serde_json::to_value(state).unwrap(),
4622 json!({"state": "unknown"})
4623 );
4624 }
4625 assert!(serde_json::from_value::<OtherStateUpdate>(json!({"state": "unknown"})).is_err());
4626 }
4627
4628 #[cfg(not(feature = "unstable_subagents"))]
4629 #[test]
4630 fn unknown_activity_is_preserved_without_subagents_feature() {
4631 let wire = serde_json::json!({
4632 "sessionUpdate": "state_update",
4633 "state": "unknown",
4634 "_meta": { "source": "worker" }
4635 });
4636 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4637 let SessionUpdate::StateUpdate(StateUpdate::Other(state)) = &parsed else {
4638 panic!("expected unrecognized state payload");
4639 };
4640 assert_eq!(state.state, "unknown");
4641 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4642 }
4643
4644 #[cfg(all(feature = "unstable_subagents", feature = "schemars"))]
4645 #[test]
4646 fn subagent_schema_carries_optional_nullable_state_snapshot() {
4647 use serde_json::json;
4648
4649 let schema = serde_json::to_value(schemars::schema_for!(SubagentUpdate)).unwrap();
4650 let properties = schema["properties"].as_object().unwrap();
4651 assert!(properties.contains_key("sessionId"));
4652 assert!(properties.contains_key("title"));
4653 assert!(properties.contains_key("description"));
4654 assert!(properties.contains_key("capabilities"));
4655 assert!(properties.contains_key("state"));
4656 assert!(properties.contains_key("_meta"));
4657 assert!(!properties.contains_key("name"));
4658 assert!(!properties.contains_key("task"));
4659 assert_eq!(schema["required"], json!(["sessionId"]));
4660 assert_eq!(
4661 properties["state"]["x-deserialize-default-on-error"],
4662 json!(true)
4663 );
4664 let state_variants = properties["state"]["anyOf"].as_array().unwrap();
4665 assert!(state_variants.contains(&json!({"type": "null"})));
4666 assert!(
4667 state_variants
4668 .iter()
4669 .any(|variant| variant["$ref"] == "#/$defs/StateUpdate")
4670 );
4671 let child =
4672 serde_json::to_value(schemars::schema_for!(SubagentSessionCapabilities)).unwrap();
4673 let variants = child["properties"]["cancel"]["anyOf"]
4674 .as_array()
4675 .expect("cancel must allow the capability object or null");
4676 assert!(variants.contains(&json!({"type": "null"})));
4677 assert!(
4678 variants
4679 .iter()
4680 .any(|variant| variant["type"] == "object" || variant.get("$ref").is_some())
4681 );
4682 assert!(!variants.iter().any(|variant| variant["type"] == "boolean"));
4683 let cancel =
4684 serde_json::to_value(schemars::schema_for!(SessionCancelCapabilities)).unwrap();
4685 assert_eq!(cancel["type"], "object");
4686 assert!(
4687 !child["required"]
4688 .as_array()
4689 .is_some_and(|required| required.contains(&json!("cancel")))
4690 );
4691 }
4692
4693 #[cfg(not(feature = "unstable_subagents"))]
4694 #[test]
4695 fn unsupported_subagent_update_is_preserved() {
4696 let wire = serde_json::json!({
4697 "sessionUpdate": "subagent_update",
4698 "sessionId": "sess_child",
4699 "capabilities": { "cancel": {} }
4700 });
4701 let parsed: SessionUpdate = serde_json::from_value(wire.clone()).unwrap();
4702 assert!(matches!(&parsed, SessionUpdate::Other(_)));
4703 assert_eq!(serde_json::to_value(parsed).unwrap(), wire);
4704 }
4705
4706 #[test]
4707 fn notice_preserves_wire_shape_nullable_fields_and_open_severity() {
4708 use serde_json::json;
4709
4710 let mut meta = Meta::new();
4711 meta.insert("source".into(), json!("fallback"));
4712 let v2_notice = SessionUpdate::Notice(
4713 Notice::new(NoticeSeverity::Error, "Provider degraded")
4714 .description("Requests may take longer than usual.")
4715 .meta(meta.clone()),
4716 );
4717 let expected = json!({
4718 "sessionUpdate": "notice",
4719 "severity": "error",
4720 "title": "Provider degraded",
4721 "description": "Requests may take longer than usual.",
4722 "_meta": { "source": "fallback" }
4723 });
4724 assert_eq!(serde_json::to_value(&v2_notice).unwrap(), expected);
4725
4726 let v1_notice = crate::v1::SessionUpdate::Notice(
4727 crate::v1::Notice::new(crate::v1::NoticeSeverity::Error, "Provider degraded")
4728 .description("Requests may take longer than usual.")
4729 .meta(meta),
4730 );
4731 assert_eq!(
4732 serde_json::to_value(v2_notice).unwrap(),
4733 serde_json::to_value(v1_notice).unwrap()
4734 );
4735
4736 let SessionUpdate::Notice(notice) = serde_json::from_value(json!({
4737 "sessionUpdate": "notice",
4738 "severity": "critical",
4739 "title": "Provider degraded",
4740 "description": null,
4741 "_meta": null
4742 }))
4743 .unwrap() else {
4744 panic!("expected notice");
4745 };
4746
4747 assert_eq!(
4748 notice.severity,
4749 NoticeSeverity::Other("critical".to_string())
4750 );
4751 assert_eq!(notice.description, None);
4752 assert_eq!(notice.meta, None);
4753 assert_eq!(
4754 serde_json::to_value(SessionUpdate::Notice(notice)).unwrap(),
4755 json!({
4756 "sessionUpdate": "notice",
4757 "severity": "critical",
4758 "title": "Provider degraded"
4759 })
4760 );
4761 }
4762
4763 #[test]
4764 fn malformed_known_notice_is_not_hidden_as_unknown() {
4765 use serde_json::json;
4766
4767 for malformed in [
4768 json!({
4769 "sessionUpdate": "notice",
4770 "severity": "warning",
4771 "title": 42
4772 }),
4773 json!({
4774 "sessionUpdate": "notice",
4775 "severity": 42,
4776 "title": "MCP server unavailable"
4777 }),
4778 json!({
4779 "sessionUpdate": "notice",
4780 "severity": "warning"
4781 }),
4782 json!({
4783 "sessionUpdate": "notice",
4784 "severity": "warning",
4785 "title": null
4786 }),
4787 json!({
4788 "sessionUpdate": "notice",
4789 "title": "MCP server unavailable"
4790 }),
4791 json!({
4792 "sessionUpdate": "notice",
4793 "severity": null,
4794 "title": "MCP server unavailable"
4795 }),
4796 ] {
4797 assert!(serde_json::from_str::<SessionUpdate>(&malformed.to_string()).is_err());
4798 assert!(serde_json::from_value::<SessionUpdate>(malformed).is_err());
4799 }
4800 }
4801
4802 #[test]
4803 fn notice_tolerates_invalid_optional_display_fields_and_custom_severity() {
4804 use serde_json::json;
4805
4806 let wire = json!({
4807 "sessionUpdate": "notice",
4808 "severity": "_custom",
4809 "title": "Provider degraded",
4810 "description": 42,
4811 "_meta": false
4812 });
4813 let SessionUpdate::Notice(notice) = serde_json::from_value(wire.clone()).unwrap() else {
4814 panic!("expected notice");
4815 };
4816 assert_eq!(
4817 serde_json::from_str::<SessionUpdate>(&wire.to_string()).unwrap(),
4818 SessionUpdate::Notice(notice.clone())
4819 );
4820 assert_eq!(notice.severity, NoticeSeverity::Other("_custom".into()));
4821 assert_eq!(notice.description, None);
4822 assert_eq!(notice.meta, None);
4823 assert_eq!(
4824 serde_json::to_value(notice).unwrap(),
4825 json!({"severity": "_custom", "title": "Provider degraded"})
4826 );
4827 }
4828
4829 #[cfg(feature = "schemars")]
4830 #[test]
4831 fn notice_schema_preserves_required_fields_and_excludes_unknown_fallback() {
4832 use serde_json::json;
4833
4834 let schema = serde_json::to_value(schemars::schema_for!(Notice)).unwrap();
4835 assert_eq!(schema["required"], json!(["severity", "title"]));
4836 assert_eq!(schema["properties"]["title"]["minLength"], 1);
4837 for field in ["description", "_meta"] {
4838 assert!(
4839 schema["properties"][field]["type"]
4840 .as_array()
4841 .unwrap()
4842 .contains(&json!("null"))
4843 );
4844 }
4845 let fallback = serde_json::to_value(schemars::schema_for!(OtherSessionUpdate)).unwrap();
4846 assert!(
4847 fallback["not"]["anyOf"]
4848 .as_array()
4849 .unwrap()
4850 .iter()
4851 .any(|variant| variant["properties"]["sessionUpdate"]["const"] == "notice")
4852 );
4853 }
4854
4855 #[test]
4856 fn compaction_updates_preserve_patch_and_open_status_semantics() {
4857 use serde_json::json;
4858
4859 assert_eq!(
4860 serde_json::to_value(SessionUpdate::CompactionUpdate(
4861 CompactionUpdate::new("cmp_001", CompactionStatus::Completed).summary(vec![
4862 ContentBlock::Text(crate::v2::TextContent::new("retained")),
4863 ]),
4864 ))
4865 .unwrap(),
4866 json!({
4867 "sessionUpdate": "compaction_update",
4868 "compactionId": "cmp_001",
4869 "status": "completed",
4870 "summary": [{ "type": "text", "text": "retained" }]
4871 })
4872 );
4873
4874 let SessionUpdate::CompactionUpdate(update) = serde_json::from_value(json!({
4875 "sessionUpdate": "compaction_update",
4876 "compactionId": "cmp_001",
4877 "status": "paused",
4878 "summary": null
4879 }))
4880 .unwrap() else {
4881 panic!("expected compaction update");
4882 };
4883 assert_eq!(update.status, CompactionStatus::Other("paused".into()));
4884 assert!(update.summary.is_null());
4885 assert!(update.error.is_undefined());
4886 }
4887
4888 #[test]
4889 fn malformed_known_compaction_update_is_not_hidden_as_unknown() {
4890 use serde_json::json;
4891
4892 assert!(
4893 serde_json::from_value::<SessionUpdate>(json!({
4894 "sessionUpdate": "compaction_update",
4895 "status": "completed"
4896 }))
4897 .is_err()
4898 );
4899 assert!(
4900 serde_json::from_value::<SessionUpdate>(json!({
4901 "sessionUpdate": "compaction_summary_chunk",
4902 "compactionId": "cmp_001"
4903 }))
4904 .is_err()
4905 );
4906 }
4907
4908 #[test]
4909 fn test_elicitation_capability_semantics() {
4910 use serde_json::json;
4911
4912 let unsupported: ClientCapabilities = serde_json::from_value(json!({})).unwrap();
4913 assert!(unsupported.elicitation.is_none());
4914
4915 let null: ClientCapabilities =
4916 serde_json::from_value(json!({ "elicitation": null })).unwrap();
4917 assert!(null.elicitation.is_none());
4918
4919 let malformed: ClientCapabilities =
4920 serde_json::from_value(json!({ "elicitation": false })).unwrap();
4921 assert!(malformed.elicitation.is_none());
4922
4923 let empty: ClientCapabilities =
4924 serde_json::from_value(json!({ "elicitation": {} })).unwrap();
4925 let empty = empty.elicitation.expect("present capability");
4926 assert!(!empty.supports_form());
4927 assert!(!empty.supports_url());
4928
4929 let form_only: ClientCapabilities = serde_json::from_value(json!({
4930 "elicitation": { "form": {} }
4931 }))
4932 .unwrap();
4933 let form_only = form_only.elicitation.expect("advertised capability");
4934 assert!(form_only.supports_form());
4935 assert!(!form_only.supports_url());
4936
4937 let url_only: ClientCapabilities = serde_json::from_value(json!({
4938 "elicitation": { "url": {} }
4939 }))
4940 .unwrap();
4941 let url_only = url_only.elicitation.expect("advertised capability");
4942 assert!(!url_only.supports_form());
4943 assert!(url_only.supports_url());
4944
4945 let both: ClientCapabilities = serde_json::from_value(json!({
4946 "elicitation": { "form": {}, "url": {} }
4947 }))
4948 .unwrap();
4949 let both = both.elicitation.expect("advertised capability");
4950 assert!(both.supports_form());
4951 assert!(both.supports_url());
4952 }
4953
4954 #[test]
4955 fn test_elicitation_method_routing_and_envelopes() {
4956 use serde_json::json;
4957
4958 assert_eq!(CLIENT_METHOD_NAMES.elicitation_create, "elicitation/create");
4959 assert_eq!(
4960 CLIENT_METHOD_NAMES.elicitation_complete,
4961 "elicitation/complete"
4962 );
4963
4964 let request =
4965 AgentRequest::CreateElicitationRequest(Box::new(CreateElicitationRequest::new(
4966 crate::v2::ElicitationFormMode::new(
4967 crate::v2::ElicitationSessionScope::new("sess_1"),
4968 crate::v2::ElicitationSchema::new(),
4969 ),
4970 "Choose a value",
4971 )));
4972 assert_eq!(request.method(), "elicitation/create");
4973 let method = Arc::from(request.method());
4974 let request = crate::v2::JsonRpcMessage::wrap(crate::v2::Request {
4975 id: crate::v2::RequestId::Number(7),
4976 method,
4977 params: Some(request),
4978 });
4979 assert_eq!(
4980 serde_json::to_value(request).unwrap(),
4981 json!({
4982 "jsonrpc": "2.0",
4983 "id": 7,
4984 "method": "elicitation/create",
4985 "params": {
4986 "mode": "form",
4987 "sessionId": "sess_1",
4988 "message": "Choose a value",
4989 "requestedSchema": { "type": "object", "properties": {} }
4990 }
4991 })
4992 );
4993
4994 let notification = AgentNotification::CompleteElicitationNotification(Box::new(
4995 CompleteElicitationNotification::new("elic_1"),
4996 ));
4997 assert_eq!(notification.method(), "elicitation/complete");
4998 let method = Arc::from(notification.method());
4999 let notification = crate::v2::JsonRpcMessage::wrap(crate::v2::Notification {
5000 method,
5001 params: Some(notification),
5002 });
5003 assert_eq!(
5004 serde_json::to_value(notification).unwrap(),
5005 json!({
5006 "jsonrpc": "2.0",
5007 "method": "elicitation/complete",
5008 "params": { "elicitationId": "elic_1" }
5009 })
5010 );
5011 }
5012
5013 #[test]
5014 fn test_client_capabilities_auth_defaults_on_malformed_value() {
5015 use serde_json::json;
5016
5017 let capabilities: ClientCapabilities = serde_json::from_value(json!({
5018 "auth": false
5019 }))
5020 .unwrap();
5021
5022 assert_eq!(capabilities.auth, None);
5023 }
5024
5025 #[test]
5026 fn test_serialization_behavior() {
5027 use serde_json::json;
5028
5029 assert_eq!(
5030 serde_json::from_value::<SessionInfoUpdate>(json!({})).unwrap(),
5031 SessionInfoUpdate {
5032 title: MaybeUndefined::Undefined,
5033 updated_at: MaybeUndefined::Undefined,
5034 meta: MaybeUndefined::Undefined
5035 }
5036 );
5037 assert_eq!(
5038 serde_json::from_value::<SessionInfoUpdate>(json!({"title": null, "updatedAt": null}))
5039 .unwrap(),
5040 SessionInfoUpdate {
5041 title: MaybeUndefined::Null,
5042 updated_at: MaybeUndefined::Null,
5043 meta: MaybeUndefined::Undefined
5044 }
5045 );
5046 assert_eq!(
5047 serde_json::from_value::<SessionInfoUpdate>(
5048 json!({"title": "title", "updatedAt": "timestamp"})
5049 )
5050 .unwrap(),
5051 SessionInfoUpdate {
5052 title: MaybeUndefined::Value("title".to_string()),
5053 updated_at: MaybeUndefined::Value("timestamp".to_string()),
5054 meta: MaybeUndefined::Undefined
5055 }
5056 );
5057
5058 let clear_meta =
5059 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": null})).unwrap();
5060 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
5061
5062 let mut meta = Meta::new();
5063 meta.insert("source".to_string(), json!("session-info"));
5064
5065 assert_eq!(
5066 serde_json::from_value::<SessionInfoUpdate>(json!({"_meta": {
5067 "source": "session-info"
5068 }}))
5069 .unwrap()
5070 .meta,
5071 MaybeUndefined::Value(meta.clone())
5072 );
5073
5074 assert_eq!(
5075 serde_json::to_value(SessionInfoUpdate::new()).unwrap(),
5076 json!({})
5077 );
5078
5079 assert_eq!(
5080 serde_json::to_value(SessionInfoUpdate::new().meta(None::<Meta>)).unwrap(),
5081 json!({"_meta": null})
5082 );
5083
5084 assert_eq!(
5085 serde_json::to_value(SessionInfoUpdate::new().meta(meta)).unwrap(),
5086 json!({"_meta": {
5087 "source": "session-info"
5088 }})
5089 );
5090 assert_eq!(
5091 serde_json::to_value(SessionInfoUpdate::new().title("title")).unwrap(),
5092 json!({"title": "title"})
5093 );
5094 assert_eq!(
5095 serde_json::to_value(SessionInfoUpdate::new().title(None)).unwrap(),
5096 json!({"title": null})
5097 );
5098 assert_eq!(
5099 serde_json::to_value(
5100 SessionInfoUpdate::new()
5101 .title("title")
5102 .title(MaybeUndefined::Undefined)
5103 )
5104 .unwrap(),
5105 json!({})
5106 );
5107 }
5108
5109 #[test]
5110 fn test_content_chunk_message_id_serialization() {
5111 use serde_json::json;
5112
5113 assert_eq!(
5114 serde_json::to_value(SessionUpdate::AgentMessageChunk(ContentChunk::new(
5115 ContentBlock::Text(crate::v2::TextContent::new("Hello")),
5116 "msg_agent_c42b9",
5117 )))
5118 .unwrap(),
5119 json!({
5120 "sessionUpdate": "agent_message_chunk",
5121 "messageId": "msg_agent_c42b9",
5122 "content": {
5123 "type": "text",
5124 "text": "Hello"
5125 }
5126 })
5127 );
5128
5129 let err = serde_json::from_value::<ContentChunk>(json!({
5130 "content": {
5131 "type": "text",
5132 "text": "Hello"
5133 }
5134 }))
5135 .unwrap_err();
5136
5137 assert!(err.to_string().contains("messageId"), "{err}");
5138 }
5139
5140 #[test]
5141 fn test_tool_call_content_chunk_serialization() {
5142 use serde_json::json;
5143
5144 assert_eq!(
5145 serde_json::to_value(SessionUpdate::ToolCallContentChunk(
5146 ToolCallContentChunk::new(
5147 "call_001",
5148 crate::v2::ContentBlock::Text(crate::v2::TextContent::new("partial output")),
5149 )
5150 ))
5151 .unwrap(),
5152 json!({
5153 "sessionUpdate": "tool_call_content_chunk",
5154 "toolCallId": "call_001",
5155 "content": {
5156 "type": "content",
5157 "content": {
5158 "type": "text",
5159 "text": "partial output"
5160 }
5161 }
5162 })
5163 );
5164
5165 let err = serde_json::from_value::<ToolCallContentChunk>(json!({
5166 "content": {
5167 "type": "content",
5168 "content": {
5169 "type": "text",
5170 "text": "partial output"
5171 }
5172 }
5173 }))
5174 .unwrap_err();
5175
5176 assert!(err.to_string().contains("toolCallId"), "{err}");
5177 }
5178
5179 #[test]
5180 fn test_full_message_serialization() {
5181 use serde_json::json;
5182
5183 assert_eq!(
5184 serde_json::to_value(SessionUpdate::UserMessage(
5185 UserMessage::new("msg_user_8f7a1").content(vec![ContentBlock::Text(
5186 crate::v2::TextContent::new("Hello")
5187 )])
5188 ))
5189 .unwrap(),
5190 json!({
5191 "sessionUpdate": "user_message",
5192 "messageId": "msg_user_8f7a1",
5193 "content": [
5194 {
5195 "type": "text",
5196 "text": "Hello"
5197 }
5198 ]
5199 })
5200 );
5201
5202 assert_eq!(
5203 serde_json::to_value(SessionUpdate::AgentMessage(
5204 AgentMessage::new("msg_agent_c42b9").content(vec![ContentBlock::Text(
5205 crate::v2::TextContent::new("Hello")
5206 )])
5207 ))
5208 .unwrap(),
5209 json!({
5210 "sessionUpdate": "agent_message",
5211 "messageId": "msg_agent_c42b9",
5212 "content": [
5213 {
5214 "type": "text",
5215 "text": "Hello"
5216 }
5217 ]
5218 })
5219 );
5220
5221 assert_eq!(
5222 serde_json::to_value(SessionUpdate::AgentThought(
5223 AgentThought::new("msg_thought_a12").content(vec![ContentBlock::Text(
5224 crate::v2::TextContent::new("Need to inspect the call sites first.")
5225 )])
5226 ))
5227 .unwrap(),
5228 json!({
5229 "sessionUpdate": "agent_thought",
5230 "messageId": "msg_thought_a12",
5231 "content": [
5232 {
5233 "type": "text",
5234 "text": "Need to inspect the call sites first."
5235 }
5236 ]
5237 })
5238 );
5239 }
5240
5241 #[test]
5242 fn test_message_upsert_serialization() {
5243 use serde_json::json;
5244
5245 assert_eq!(
5246 serde_json::to_value(SessionUpdate::UserMessage(
5247 UserMessage::new("msg_empty").content(Vec::<ContentBlock>::new())
5248 ))
5249 .unwrap(),
5250 json!({
5251 "sessionUpdate": "user_message",
5252 "messageId": "msg_empty",
5253 "content": []
5254 })
5255 );
5256
5257 let empty = serde_json::from_value::<UserMessage>(json!({
5258 "messageId": "msg_empty",
5259 "content": []
5260 }))
5261 .unwrap();
5262 assert!(matches!(
5263 empty.content,
5264 MaybeUndefined::Value(ref content) if content.is_empty()
5265 ));
5266
5267 let patch = serde_json::from_value::<AgentMessage>(json!({
5268 "messageId": "msg_agent_c42b9"
5269 }))
5270 .unwrap();
5271 assert_eq!(patch.content, MaybeUndefined::Undefined);
5272 assert_eq!(patch.meta, MaybeUndefined::Undefined);
5273
5274 let malformed_meta = serde_json::from_value::<AgentMessage>(json!({
5275 "messageId": "msg_agent_c42b9",
5276 "_meta": false
5277 }))
5278 .unwrap();
5279 assert_eq!(malformed_meta.meta, MaybeUndefined::Undefined);
5280
5281 let patch = serde_json::from_value::<AgentThought>(json!({
5282 "messageId": "msg_thought_a12"
5283 }))
5284 .unwrap();
5285 assert_eq!(patch.content, MaybeUndefined::Undefined);
5286
5287 let clear = serde_json::from_value::<UserMessage>(json!({
5288 "messageId": "msg_user_8f7a1",
5289 "content": null
5290 }))
5291 .unwrap();
5292 assert_eq!(clear.content, MaybeUndefined::Null);
5293
5294 let clear_meta = serde_json::from_value::<UserMessage>(json!({
5295 "messageId": "msg_user_8f7a1",
5296 "_meta": null
5297 }))
5298 .unwrap();
5299 assert_eq!(clear_meta.meta, MaybeUndefined::Null);
5300
5301 let mut meta = Meta::new();
5302 meta.insert("source".to_string(), json!("replay"));
5303
5304 assert_eq!(
5305 serde_json::to_value(SessionUpdate::UserMessage(
5306 UserMessage::new("msg_user_8f7a1").meta(meta)
5307 ))
5308 .unwrap(),
5309 json!({
5310 "sessionUpdate": "user_message",
5311 "messageId": "msg_user_8f7a1",
5312 "_meta": {
5313 "source": "replay"
5314 }
5315 })
5316 );
5317
5318 assert_eq!(
5319 serde_json::to_value(SessionUpdate::UserMessage(
5320 UserMessage::new("msg_user_8f7a1").meta(None::<Meta>)
5321 ))
5322 .unwrap(),
5323 json!({
5324 "sessionUpdate": "user_message",
5325 "messageId": "msg_user_8f7a1",
5326 "_meta": null
5327 })
5328 );
5329 }
5330
5331 #[test]
5332 fn test_usage_update_serialization() {
5333 use serde_json::json;
5334
5335 assert_eq!(
5336 serde_json::to_value(SessionUpdate::UsageUpdate(UsageUpdate::new(
5337 53_000, 200_000
5338 )))
5339 .unwrap(),
5340 json!({
5341 "sessionUpdate": "usage_update",
5342 "used": 53000,
5343 "size": 200_000
5344 })
5345 );
5346
5347 assert_eq!(
5348 serde_json::to_value(SessionUpdate::UsageUpdate(
5349 UsageUpdate::new(53_000, 200_000).cost(Cost::new(0.045, "USD"))
5350 ))
5351 .unwrap(),
5352 json!({
5353 "sessionUpdate": "usage_update",
5354 "used": 53000,
5355 "size": 200_000,
5356 "cost": {
5357 "amount": 0.045,
5358 "currency": "USD"
5359 }
5360 })
5361 );
5362
5363 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
5364 "sessionUpdate": "usage_update",
5365 "used": 53000,
5366 "size": 200_000,
5367 "cost": null
5368 }))
5369 .unwrap() else {
5370 panic!("expected usage update");
5371 };
5372
5373 assert_eq!(update.cost, MaybeUndefined::Null);
5374
5375 let SessionUpdate::UsageUpdate(update) = serde_json::from_value(json!({
5376 "sessionUpdate": "usage_update",
5377 "used": 53000,
5378 "size": 200_000
5379 }))
5380 .unwrap() else {
5381 panic!("expected usage update");
5382 };
5383
5384 assert_eq!(update.cost, MaybeUndefined::Undefined);
5385 }
5386
5387 #[test]
5388 fn error_stop_reason_carries_failure_beside_discriminator() {
5389 use crate::v2::{Error, ErrorStopReason};
5390 use serde_json::json;
5391
5392 let update =
5393 SessionUpdate::StateUpdate(StateUpdate::Idle(IdleStateUpdate::new().stop_reason(
5394 StopReason::from(ErrorStopReason::new().error(Error::auth_required())),
5395 )));
5396 let wire = json!({
5397 "sessionUpdate": "state_update",
5398 "state": "idle",
5399 "stopReason": "error",
5400 "error": { "code": -32000, "message": "Authentication required" }
5401 });
5402 assert_eq!(serde_json::to_value(&update).unwrap(), wire);
5403 assert_eq!(
5404 serde_json::from_value::<SessionUpdate>(wire).unwrap(),
5405 update
5406 );
5407
5408 let with_data =
5409 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::from(
5410 ErrorStopReason::new().error(
5411 Error::new(-32099, "Provider unavailable").data(json!({ "status": 503 })),
5412 ),
5413 )));
5414 let wire = json!({
5415 "state": "idle",
5416 "stopReason": "error",
5417 "error": {
5418 "code": -32099,
5419 "message": "Provider unavailable",
5420 "data": { "status": 503 }
5421 }
5422 });
5423 assert_eq!(serde_json::to_value(&with_data).unwrap(), wire);
5424 assert_eq!(
5425 serde_json::from_value::<StateUpdate>(wire).unwrap(),
5426 with_data
5427 );
5428
5429 let without_details = StateUpdate::Idle(
5432 IdleStateUpdate::new().stop_reason(StopReason::from(ErrorStopReason::new())),
5433 );
5434 for error in [
5435 None,
5436 Some(json!(null)),
5437 Some(json!("failed")),
5438 Some(json!({ "message": "missing code" })),
5439 ] {
5440 let mut wire = json!({ "state": "idle", "stopReason": "error" });
5441 if let Some(error) = error {
5442 wire["error"] = error;
5443 }
5444 assert_eq!(
5445 serde_json::from_value::<StateUpdate>(wire).unwrap(),
5446 without_details
5447 );
5448 }
5449 assert_eq!(
5450 serde_json::to_value(&without_details).unwrap(),
5451 json!({ "state": "idle", "stopReason": "error" })
5452 );
5453
5454 let parsed: StateUpdate = serde_json::from_value(json!({
5458 "state": "idle",
5459 "stopReason": "end_turn",
5460 "error": { "code": -32603, "message": "Internal error" }
5461 }))
5462 .unwrap();
5463 assert_eq!(
5464 parsed,
5465 StateUpdate::Idle(IdleStateUpdate::new().stop_reason(StopReason::EndTurn))
5466 );
5467 }
5468
5469 #[test]
5470 fn flattened_stop_reason_keeps_open_enum_tolerance() {
5471 use crate::v2::OtherStopReason;
5472 use serde_json::json;
5473
5474 let meta: Meta = [("source".into(), json!("test"))].into_iter().collect();
5475
5476 for stop_reason in [None, Some(json!(null)), Some(json!(true)), Some(json!({}))] {
5479 let mut wire = json!({ "state": "idle", "_meta": { "source": "test" } });
5480 if let Some(stop_reason) = stop_reason {
5481 wire["stopReason"] = stop_reason;
5482 }
5483 let StateUpdate::Idle(idle) = serde_json::from_value(wire).unwrap() else {
5484 panic!("expected idle state update");
5485 };
5486 assert_eq!(idle.stop_reason, None);
5487 assert_eq!(idle.meta, Some(meta.clone()));
5488 }
5489
5490 let wire = json!({
5493 "state": "idle",
5494 "stopReason": "_paused",
5495 "resumeAfter": 30,
5496 "_meta": { "source": "test" }
5497 });
5498 let parsed: StateUpdate = serde_json::from_value(wire.clone()).unwrap();
5499 let expected = StateUpdate::Idle(
5500 IdleStateUpdate::new()
5501 .stop_reason(StopReason::Other(OtherStopReason::new(
5502 "_paused",
5503 [("resumeAfter".into(), json!(30))].into_iter().collect(),
5504 )))
5505 .meta(meta),
5506 );
5507 assert_eq!(parsed, expected);
5508 assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
5509 }
5510
5511 #[test]
5512 fn known_stop_reasons_are_never_read_as_other() {
5513 use crate::v2::{ErrorStopReason, OtherStopReason};
5514 use serde_json::json;
5515
5516 let known = [
5517 StopReason::EndTurn,
5518 StopReason::MaxTokens,
5519 StopReason::MaxTurnRequests,
5520 StopReason::Refusal,
5521 StopReason::Cancelled,
5522 StopReason::Error(ErrorStopReason::new()),
5523 ];
5524 for stop_reason in &known {
5526 match stop_reason {
5527 StopReason::EndTurn
5528 | StopReason::MaxTokens
5529 | StopReason::MaxTurnRequests
5530 | StopReason::Refusal
5531 | StopReason::Cancelled
5532 | StopReason::Error(_)
5533 | StopReason::Other(_) => {}
5534 }
5535 }
5536
5537 for stop_reason in known {
5538 let update = StateUpdate::Idle(IdleStateUpdate::new().stop_reason(stop_reason));
5539 let wire = serde_json::to_value(&update).unwrap();
5540 assert_eq!(
5541 serde_json::from_value::<StateUpdate>(wire.clone()).unwrap(),
5542 update
5543 );
5544 assert!(
5545 serde_json::from_value::<OtherStopReason>(
5546 json!({ "stopReason": wire["stopReason"] })
5547 )
5548 .is_err(),
5549 "{} must not be accepted as a custom stop reason",
5550 wire["stopReason"]
5551 );
5552 }
5553 }
5554
5555 #[test]
5556 fn unknown_stop_reason_leaves_usage_on_the_idle_update() {
5557 use serde_json::json;
5558
5559 let usage = json!({ "totalTokens": 10, "inputTokens": 6, "outputTokens": 4 });
5560 let wire = json!({ "state": "idle", "stopReason": "_paused", "usage": usage });
5561 let StateUpdate::Idle(idle) = serde_json::from_value(wire.clone()).unwrap() else {
5562 panic!("expected idle state update");
5563 };
5564 let Some(StopReason::Other(other)) = &idle.stop_reason else {
5565 panic!("expected a custom stop reason");
5566 };
5567 assert!(other.fields.is_empty());
5568
5569 #[cfg(feature = "unstable_end_turn_token_usage")]
5570 {
5571 assert!(idle.usage.is_some());
5572 assert_eq!(serde_json::to_value(&idle).unwrap()["usage"], usage);
5573 }
5574 #[cfg(not(feature = "unstable_end_turn_token_usage"))]
5577 assert!(serde_json::to_value(&idle).unwrap().get("usage").is_none());
5578 }
5579
5580 #[test]
5581 fn test_state_update_serialization() {
5582 use serde_json::json;
5583
5584 assert_eq!(
5585 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Running(
5586 RunningStateUpdate::new()
5587 )))
5588 .unwrap(),
5589 json!({
5590 "sessionUpdate": "state_update",
5591 "state": "running"
5592 })
5593 );
5594
5595 assert_eq!(
5596 serde_json::to_value(SessionUpdate::StateUpdate(StateUpdate::Idle(
5597 IdleStateUpdate::new().stop_reason(StopReason::EndTurn)
5598 )))
5599 .unwrap(),
5600 json!({
5601 "sessionUpdate": "state_update",
5602 "state": "idle",
5603 "stopReason": "end_turn"
5604 })
5605 );
5606
5607 let SessionUpdate::StateUpdate(update) = serde_json::from_value(json!({
5608 "sessionUpdate": "state_update",
5609 "state": "requires_action"
5610 }))
5611 .unwrap() else {
5612 panic!("expected state update");
5613 };
5614
5615 assert!(matches!(update, StateUpdate::RequiresAction(_)));
5616
5617 let SessionUpdate::StateUpdate(StateUpdate::Idle(update)) = serde_json::from_value(json!({
5618 "sessionUpdate": "state_update",
5619 "state": "idle",
5620 "stopReason": null
5621 }))
5622 .unwrap() else {
5623 panic!("expected idle state update");
5624 };
5625
5626 assert_eq!(update.stop_reason, None);
5627
5628 let SessionUpdate::StateUpdate(StateUpdate::Other(update)) =
5629 serde_json::from_value(json!({
5630 "sessionUpdate": "state_update",
5631 "state": "_paused",
5632 "label": "Paused"
5633 }))
5634 .unwrap()
5635 else {
5636 panic!("expected unknown state update");
5637 };
5638
5639 assert_eq!(update.state, "_paused");
5640 assert_eq!(update.fields["label"], json!("Paused"));
5641 }
5642
5643 #[test]
5644 fn session_update_preserves_unknown_variant() {
5645 use serde_json::json;
5646
5647 let update: SessionUpdate = serde_json::from_value(json!({
5648 "sessionUpdate": "_status_badge",
5649 "label": "Indexing",
5650 "progress": 0.5
5651 }))
5652 .unwrap();
5653
5654 let SessionUpdate::Other(unknown) = update else {
5655 panic!("expected unknown session update");
5656 };
5657
5658 assert_eq!(unknown.session_update, "_status_badge");
5659 assert_eq!(unknown.fields.get("label"), Some(&json!("Indexing")));
5660 assert_eq!(unknown.fields.get("progress"), Some(&json!(0.5)));
5661
5662 assert_eq!(
5663 serde_json::to_value(SessionUpdate::Other(unknown)).unwrap(),
5664 json!({
5665 "sessionUpdate": "_status_badge",
5666 "label": "Indexing",
5667 "progress": 0.5
5668 })
5669 );
5670 }
5671
5672 #[test]
5673 fn terminal_session_updates_use_known_discriminators() {
5674 use serde_json::json;
5675
5676 assert_eq!(
5677 serde_json::to_value(SessionUpdate::TerminalUpdate(
5678 TerminalUpdate::new("term_1").command("cargo test")
5679 ))
5680 .unwrap(),
5681 json!({
5682 "sessionUpdate": "terminal_update",
5683 "terminalId": "term_1",
5684 "command": "cargo test"
5685 })
5686 );
5687 assert_eq!(
5688 serde_json::to_value(SessionUpdate::TerminalOutputChunk(
5689 TerminalOutputChunk::new("term_1", "dGVzdAo=")
5690 ))
5691 .unwrap(),
5692 json!({
5693 "sessionUpdate": "terminal_output_chunk",
5694 "terminalId": "term_1",
5695 "data": "dGVzdAo="
5696 })
5697 );
5698 }
5699
5700 #[test]
5701 fn session_update_does_not_hide_malformed_known_terminal_variants() {
5702 use serde_json::json;
5703
5704 assert!(
5705 serde_json::from_value::<SessionUpdate>(json!({
5706 "sessionUpdate": "terminal_update"
5707 }))
5708 .is_err()
5709 );
5710 assert!(
5711 serde_json::from_value::<SessionUpdate>(json!({
5712 "sessionUpdate": "terminal_output_chunk",
5713 "terminalId": "term_1"
5714 }))
5715 .is_err()
5716 );
5717 }
5718
5719 #[test]
5720 fn test_plan_update_serialization() {
5721 use serde_json::json;
5722
5723 let plan_update =
5724 SessionUpdate::PlanUpdate(PlanUpdate::new(crate::v2::PlanUpdateContent::items(
5725 "plan-1",
5726 vec![crate::v2::PlanEntry::new(
5727 "Step 1",
5728 crate::v2::PlanEntryPriority::High,
5729 crate::v2::PlanEntryStatus::Pending,
5730 )],
5731 )));
5732
5733 assert_eq!(
5734 serde_json::to_value(plan_update).unwrap(),
5735 json!({
5736 "sessionUpdate": "plan_update",
5737 "plan": {
5738 "type": "items",
5739 "planId": "plan-1",
5740 "entries": [
5741 {
5742 "content": "Step 1",
5743 "priority": "high",
5744 "status": "pending"
5745 }
5746 ]
5747 }
5748 })
5749 );
5750 }
5751
5752 #[cfg(feature = "unstable_plan_operations")]
5753 #[test]
5754 fn test_plan_removed_serialization() {
5755 use serde_json::json;
5756
5757 assert_eq!(
5758 serde_json::to_value(SessionUpdate::PlanRemoved(PlanRemoved::new("plan-1"))).unwrap(),
5759 json!({
5760 "sessionUpdate": "plan_removed",
5761 "planId": "plan-1"
5762 })
5763 );
5764 }
5765
5766 #[test]
5767 fn available_command_uses_type_discriminator() {
5768 use serde_json::json;
5769
5770 let command = AvailableCommand::Command(
5771 Command::new("web", "Search the web")
5772 .input(AvailableCommandInput::Text(TextCommandInput::new("Query"))),
5773 );
5774
5775 let json = serde_json::to_value(&command).unwrap();
5776 assert_eq!(
5777 json,
5778 json!({
5779 "type": "command",
5780 "name": "web",
5781 "description": "Search the web",
5782 "input": { "type": "text", "hint": "Query" }
5783 })
5784 );
5785
5786 let roundtripped: AvailableCommand = serde_json::from_value(json).unwrap();
5787 assert_eq!(roundtripped, command);
5788 assert_eq!(roundtripped.name(), "web");
5789 assert_eq!(roundtripped.description(), "Search the web");
5790 }
5791
5792 #[test]
5793 fn available_command_preserves_unknown_typed_variant() {
5794 use serde_json::json;
5795
5796 let payload = json!({
5797 "type": "_skill",
5798 "name": "pdf-processing",
5799 "description": "Extract text from PDFs",
5800 "uri": "skill://pdf-processing/SKILL.md",
5801 "_meta": { "example.com/source": "mcp" }
5802 });
5803 let command: AvailableCommand = serde_json::from_value(payload.clone()).unwrap();
5804
5805 assert_eq!(command.name(), "pdf-processing");
5806 assert_eq!(command.description(), "Extract text from PDFs");
5807 assert!(command.meta().is_some());
5808
5809 let AvailableCommand::Other(unknown) = &command else {
5810 panic!("expected unknown command");
5811 };
5812 assert_eq!(unknown.type_, "_skill");
5813 assert_eq!(
5814 unknown.fields.get("uri"),
5815 Some(&json!("skill://pdf-processing/SKILL.md"))
5816 );
5817 assert_eq!(serde_json::to_value(&command).unwrap(), payload);
5818 }
5819
5820 #[test]
5821 fn available_command_unknown_tolerates_invalid_meta() {
5822 use serde_json::json;
5823
5824 let command: AvailableCommand = serde_json::from_value(json!({
5825 "type": "_skill",
5826 "name": "pdf-processing",
5827 "description": "Extract text from PDFs",
5828 "_meta": "oops"
5829 }))
5830 .unwrap();
5831
5832 assert!(matches!(command, AvailableCommand::Other(_)));
5833 assert!(command.meta().is_none());
5834 }
5835
5836 #[test]
5837 fn available_command_unknown_does_not_hide_malformed_command_variant() {
5838 use serde_json::json;
5839
5840 assert!(
5842 serde_json::from_value::<AvailableCommand>(json!({
5843 "name": "test",
5844 "description": "Run project tests"
5845 }))
5846 .is_err()
5847 );
5848 assert!(
5849 serde_json::from_value::<AvailableCommand>(json!({
5850 "type": "command",
5851 "name": "test"
5852 }))
5853 .is_err()
5854 );
5855 assert!(
5856 serde_json::from_value::<OtherAvailableCommand>(json!({
5857 "type": "command",
5858 "name": "test",
5859 "description": "Run project tests"
5860 }))
5861 .is_err()
5862 );
5863 assert!(
5864 serde_json::from_value::<AvailableCommand>(json!({
5865 "type": "_skill",
5866 "name": "pdf-processing"
5867 }))
5868 .is_err()
5869 );
5870 }
5871
5872 #[test]
5873 fn available_command_input_preserves_unknown_typed_variant() {
5874 use serde_json::json;
5875
5876 let input: AvailableCommandInput = serde_json::from_value(json!({
5877 "type": "_choices",
5878 "hint": "Pick one",
5879 "options": ["fast", "careful"]
5880 }))
5881 .unwrap();
5882
5883 let AvailableCommandInput::Other(unknown) = input else {
5884 panic!("expected unknown command input");
5885 };
5886
5887 assert_eq!(unknown.type_, "_choices");
5888 assert_eq!(unknown.fields.get("hint"), Some(&json!("Pick one")));
5889 assert_eq!(
5890 unknown.fields.get("options"),
5891 Some(&json!(["fast", "careful"]))
5892 );
5893 assert_eq!(
5894 serde_json::to_value(AvailableCommandInput::Other(unknown)).unwrap(),
5895 json!({
5896 "type": "_choices",
5897 "hint": "Pick one",
5898 "options": ["fast", "careful"]
5899 })
5900 );
5901 }
5902
5903 #[test]
5904 fn available_command_input_text_uses_type_discriminator() {
5905 use serde_json::json;
5906
5907 let input = AvailableCommandInput::Text(TextCommandInput::new("Describe changes"));
5908
5909 let json = serde_json::to_value(&input).unwrap();
5910 assert_eq!(
5911 json,
5912 json!({
5913 "type": "text",
5914 "hint": "Describe changes"
5915 })
5916 );
5917
5918 let roundtripped: AvailableCommandInput = serde_json::from_value(json).unwrap();
5919 assert!(matches!(roundtripped, AvailableCommandInput::Text(_)));
5920 }
5921
5922 #[test]
5923 fn request_permission_subject_tool_call_uses_type_discriminator() {
5924 use serde_json::json;
5925
5926 let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001"));
5927
5928 let json = serde_json::to_value(&subject).unwrap();
5929 assert_eq!(
5930 json,
5931 json!({
5932 "type": "tool_call",
5933 "toolCall": {
5934 "toolCallId": "call_001"
5935 }
5936 })
5937 );
5938
5939 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5940 assert!(matches!(
5941 roundtripped,
5942 RequestPermissionSubject::ToolCall(_)
5943 ));
5944 }
5945
5946 #[test]
5947 fn request_permission_subject_command_uses_type_discriminator() {
5948 use serde_json::json;
5949
5950 let mut meta = Meta::new();
5951 meta.insert("source".to_string(), json!("shell"));
5952 let subject = RequestPermissionSubject::from(
5953 CommandPermissionSubject::new("cargo test", "/workspace/project")
5954 .tool_call_id("call_001")
5955 .terminal_id("term_1")
5956 .meta(meta),
5957 );
5958
5959 let json = serde_json::to_value(&subject).unwrap();
5960 assert_eq!(
5961 json,
5962 json!({
5963 "type": "command",
5964 "command": "cargo test",
5965 "cwd": "/workspace/project",
5966 "toolCallId": "call_001",
5967 "terminalId": "term_1",
5968 "_meta": {
5969 "source": "shell"
5970 }
5971 })
5972 );
5973
5974 let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap();
5975 assert!(matches!(roundtripped, RequestPermissionSubject::Command(_)));
5976 }
5977
5978 #[test]
5979 fn command_permission_subject_treats_optional_association_nulls_as_omitted() {
5980 use serde_json::json;
5981
5982 let subject: RequestPermissionSubject = serde_json::from_value(json!({
5983 "type": "command",
5984 "command": "cargo test",
5985 "cwd": "/workspace/project",
5986 "toolCallId": null,
5987 "terminalId": null,
5988 "_meta": null
5989 }))
5990 .unwrap();
5991
5992 let RequestPermissionSubject::Command(subject) = subject else {
5993 panic!("expected command permission subject");
5994 };
5995 assert_eq!(subject.cwd, AbsolutePath::new("/workspace/project"));
5996 assert_eq!(subject.tool_call_id, None);
5997 assert_eq!(subject.terminal_id, None);
5998 assert_eq!(subject.meta, None);
5999 assert_eq!(
6000 serde_json::to_value(RequestPermissionSubject::Command(subject)).unwrap(),
6001 json!({
6002 "type": "command",
6003 "command": "cargo test",
6004 "cwd": "/workspace/project"
6005 })
6006 );
6007 }
6008
6009 #[test]
6010 fn request_permission_subject_preserves_unknown_variant() {
6011 use serde_json::json;
6012
6013 let subject: RequestPermissionSubject = serde_json::from_value(json!({
6014 "type": "_review",
6015 "reason": "needs-review",
6016 "retryAfterSeconds": 30
6017 }))
6018 .unwrap();
6019
6020 let RequestPermissionSubject::Other(unknown) = subject else {
6021 panic!("expected unknown permission subject");
6022 };
6023
6024 assert_eq!(unknown.type_, "_review");
6025 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
6026 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
6027 assert_eq!(
6028 serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(),
6029 json!({
6030 "type": "_review",
6031 "reason": "needs-review",
6032 "retryAfterSeconds": 30
6033 })
6034 );
6035 }
6036
6037 #[test]
6038 fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() {
6039 use serde_json::json;
6040
6041 assert!(
6042 serde_json::from_value::<RequestPermissionSubject>(json!({
6043 "type": "tool_call"
6044 }))
6045 .is_err()
6046 );
6047 assert!(
6048 serde_json::from_value::<RequestPermissionSubject>(json!({
6049 "type": 1
6050 }))
6051 .is_err()
6052 );
6053 assert!(
6054 serde_json::from_value::<RequestPermissionSubject>(json!({
6055 "type": "command",
6056 "cwd": "/workspace/project"
6057 }))
6058 .is_err()
6059 );
6060 assert!(
6061 serde_json::from_value::<RequestPermissionSubject>(json!({
6062 "type": "command",
6063 "command": "cargo test"
6064 }))
6065 .is_err()
6066 );
6067 assert!(
6068 serde_json::from_value::<RequestPermissionSubject>(json!({
6069 "type": "command",
6070 "command": "cargo test",
6071 "cwd": null
6072 }))
6073 .is_err()
6074 );
6075 }
6076
6077 #[test]
6078 fn request_permission_title_and_description_are_separate_from_tool_call_content() {
6079 use serde_json::json;
6080
6081 let request =
6082 RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new())
6083 .description("Allow this tool to edit src/main.rs?")
6084 .subject(RequestPermissionSubject::from(ToolCallUpdate::new(
6085 "call_001",
6086 )));
6087
6088 assert_eq!(
6089 serde_json::to_value(request).unwrap(),
6090 json!({
6091 "sessionId": "sess_abc123def456",
6092 "title": "Approve file edit?",
6093 "description": "Allow this tool to edit src/main.rs?",
6094 "subject": {
6095 "type": "tool_call",
6096 "toolCall": {
6097 "toolCallId": "call_001"
6098 }
6099 },
6100 "options": []
6101 })
6102 );
6103 }
6104
6105 #[test]
6106 fn request_permission_requires_title_and_allows_missing_subject() {
6107 use serde_json::json;
6108
6109 let request = RequestPermissionRequest::new(
6110 "sess_abc123def456",
6111 "Approve elevated permissions?",
6112 Vec::new(),
6113 );
6114
6115 assert_eq!(
6116 serde_json::to_value(request).unwrap(),
6117 json!({
6118 "sessionId": "sess_abc123def456",
6119 "title": "Approve elevated permissions?",
6120 "options": []
6121 })
6122 );
6123
6124 let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({
6125 "sessionId": "sess_abc123def456",
6126 "title": "Approve elevated permissions?",
6127 "options": []
6128 }))
6129 .unwrap();
6130 assert!(missing_subject.subject.is_none());
6131
6132 let null_subject: RequestPermissionRequest = serde_json::from_value(json!({
6133 "sessionId": "sess_abc123def456",
6134 "title": "Approve elevated permissions?",
6135 "subject": null,
6136 "options": []
6137 }))
6138 .unwrap();
6139 assert!(null_subject.subject.is_none());
6140
6141 assert!(
6142 serde_json::from_value::<RequestPermissionRequest>(json!({
6143 "sessionId": "sess_abc123def456",
6144 "options": []
6145 }))
6146 .is_err()
6147 );
6148 }
6149
6150 #[test]
6151 fn request_permission_outcome_preserves_unknown_variant() {
6152 use serde_json::json;
6153
6154 let outcome: RequestPermissionOutcome = serde_json::from_value(json!({
6155 "outcome": "_defer",
6156 "reason": "needs-review",
6157 "retryAfterSeconds": 30
6158 }))
6159 .unwrap();
6160
6161 let RequestPermissionOutcome::Other(unknown) = outcome else {
6162 panic!("expected unknown permission outcome");
6163 };
6164
6165 assert_eq!(unknown.outcome, "_defer");
6166 assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review")));
6167 assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30)));
6168 assert_eq!(
6169 serde_json::to_value(RequestPermissionOutcome::Other(unknown)).unwrap(),
6170 json!({
6171 "outcome": "_defer",
6172 "reason": "needs-review",
6173 "retryAfterSeconds": 30
6174 })
6175 );
6176 }
6177
6178 #[test]
6179 fn request_permission_outcome_unknown_does_not_hide_malformed_known_variant() {
6180 use serde_json::json;
6181
6182 assert!(
6183 serde_json::from_value::<RequestPermissionOutcome>(json!({
6184 "outcome": "selected"
6185 }))
6186 .is_err()
6187 );
6188 assert!(
6189 serde_json::from_value::<RequestPermissionOutcome>(json!({
6190 "outcome": 1
6191 }))
6192 .is_err()
6193 );
6194 }
6195
6196 #[test]
6197 fn available_command_input_unknown_does_not_hide_malformed_text_variant() {
6198 use serde_json::json;
6199
6200 assert!(serde_json::from_value::<AvailableCommandInput>(json!({})).is_err());
6201 assert!(
6202 serde_json::from_value::<AvailableCommandInput>(json!({
6203 "hint": "Pick one"
6204 }))
6205 .is_err()
6206 );
6207 assert!(
6208 serde_json::from_value::<AvailableCommandInput>(json!({
6209 "type": 1,
6210 "hint": "Pick one"
6211 }))
6212 .is_err()
6213 );
6214 assert!(
6215 serde_json::from_value::<OtherAvailableCommandInput>(json!({
6216 "type": "text",
6217 "hint": "Pick one"
6218 }))
6219 .is_err()
6220 );
6221 }
6222
6223 #[cfg(feature = "unstable_mcp_over_acp")]
6224 #[test]
6225 fn test_agent_mcp_request_method_names() {
6226 use serde_json::json;
6227
6228 let params: serde_json::Map<String, serde_json::Value> =
6229 [("cursor".to_string(), json!("abc"))].into_iter().collect();
6230
6231 assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message");
6232 assert_eq!(
6233 AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new(
6234 "server-1",
6235 "req-1",
6236 "tools/list"
6237 )))
6238 .method(),
6239 "mcp/message"
6240 );
6241 assert_eq!(
6242 serde_json::to_value(
6243 MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params)
6244 )
6245 .unwrap(),
6246 json!({
6247 "serverId": "server-1",
6248 "requestId": "req-1",
6249 "method": "tools/list",
6250 "params": { "cursor": "abc" }
6251 })
6252 );
6253
6254 let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({
6255 "serverId": "server-1",
6256 "requestId": "req-1",
6257 "method": "tools/list",
6258 "params": null,
6259 "_meta": null
6260 }))
6261 .unwrap();
6262 assert_eq!(request_with_null_params.params, None);
6263 assert_eq!(request_with_null_params.meta, None);
6264 for key in ["serverId", "requestId", "method"] {
6265 let mut value =
6266 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
6267 value.as_object_mut().unwrap().remove(key);
6268 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
6269 }
6270 for key in ["serverId", "requestId", "method"] {
6271 let mut value =
6272 json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"});
6273 value[key] = serde_json::Value::Null;
6274 assert!(serde_json::from_value::<MessageMcpRequest>(value).is_err());
6275 }
6276 }
6277
6278 #[test]
6279 fn test_auth_capabilities_serialize_terminal_support_as_object() {
6280 use serde_json::json;
6281
6282 let capabilities = ClientAuthCapabilities::new().terminal(TerminalAuthCapabilities::new());
6283
6284 assert_eq!(
6285 serde_json::to_value(&capabilities).unwrap(),
6286 json!({
6287 "terminal": {}
6288 })
6289 );
6290
6291 let deserialized: ClientAuthCapabilities = serde_json::from_value(json!({
6292 "terminal": false
6293 }))
6294 .unwrap();
6295 assert!(deserialized.terminal.is_none());
6296 }
6297
6298 #[test]
6299 fn request_permission_request_rejects_malformed_options() {
6300 use serde_json::json;
6301
6302 assert!(
6303 serde_json::from_value::<RequestPermissionRequest>(json!({
6304 "sessionId": "sess-1",
6305 "title": "Run tool?",
6306 "options": "not-an-array"
6307 }))
6308 .is_err()
6309 );
6310 assert!(
6311 serde_json::from_value::<RequestPermissionRequest>(json!({
6312 "sessionId": "sess-1",
6313 "title": "Run tool?",
6314 "options": [{"optionId": "allow"}]
6315 }))
6316 .is_err()
6317 );
6318 }
6319
6320 #[cfg(feature = "unstable_plan_operations")]
6321 #[test]
6322 fn malformed_plan_removed_is_not_hidden_as_unknown_update() {
6323 use serde_json::json;
6324
6325 assert!(
6326 serde_json::from_value::<SessionUpdate>(json!({
6327 "sessionUpdate": "plan_removed"
6328 }))
6329 .is_err()
6330 );
6331 }
6332}