1use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11
12use crate::client::Client;
13use crate::error::Result;
14use crate::pagination::Paginated;
15
16use super::agents::{AgentMcpServer, AgentModel, AgentTool, Skill};
17use super::betas;
18use super::resources::SessionResource;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28#[non_exhaustive]
29pub enum SessionStatus {
30 Idle,
32 Running,
34 Rescheduling,
36 Terminated,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(untagged)]
44#[non_exhaustive]
45pub enum AgentRef {
46 Latest(String),
48 Pinned {
50 #[serde(rename = "type")]
52 ty: String,
53 id: String,
55 version: u32,
57 },
58}
59
60impl AgentRef {
61 #[must_use]
63 pub fn latest(id: impl Into<String>) -> Self {
64 Self::Latest(id.into())
65 }
66
67 #[must_use]
69 pub fn pinned(id: impl Into<String>, version: u32) -> Self {
70 Self::Pinned {
71 ty: "agent".into(),
72 id: id.into(),
73 version,
74 }
75 }
76}
77
78impl From<&str> for AgentRef {
79 fn from(s: &str) -> Self {
80 Self::latest(s)
81 }
82}
83
84impl From<String> for AgentRef {
85 fn from(s: String) -> Self {
86 Self::latest(s)
87 }
88}
89
90#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
92#[non_exhaustive]
93pub struct SessionUsage {
94 #[serde(default)]
96 pub input_tokens: u64,
97 #[serde(default)]
99 pub output_tokens: u64,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub cache_creation: Option<CacheCreationUsage>,
103 #[serde(default)]
105 pub cache_read_input_tokens: u64,
106}
107
108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
110#[non_exhaustive]
111pub struct CacheCreationUsage {
112 #[serde(default)]
114 pub ephemeral_5m_input_tokens: u64,
115 #[serde(default)]
117 pub ephemeral_1h_input_tokens: u64,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122#[non_exhaustive]
123pub struct Session {
124 pub id: String,
126 #[serde(rename = "type", default = "default_session_kind")]
128 pub kind: String,
129 pub status: SessionStatus,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub agent: Option<SessionAgent>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub environment_id: Option<String>,
138 #[serde(default, skip_serializing_if = "Vec::is_empty")]
140 pub vault_ids: Vec<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub title: Option<String>,
144 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
146 pub metadata: HashMap<String, String>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub usage: Option<SessionUsage>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub stats: Option<SessionStats>,
153 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub resources: Vec<SessionResource>,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub outcome_evaluations: Vec<serde_json::Value>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub created_at: Option<String>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub updated_at: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub archived_at: Option<String>,
176}
177
178fn default_session_kind() -> String {
179 "session".to_owned()
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
187#[non_exhaustive]
188pub struct SessionAgent {
189 #[serde(rename = "type", default = "default_session_agent_kind")]
191 pub kind: String,
192 pub id: String,
194 pub version: u32,
196 pub name: String,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub description: Option<String>,
201 pub model: AgentModel,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub system: Option<String>,
206 #[serde(default, skip_serializing_if = "Vec::is_empty")]
208 pub tools: Vec<AgentTool>,
209 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub mcp_servers: Vec<AgentMcpServer>,
212 #[serde(default, skip_serializing_if = "Vec::is_empty")]
214 pub skills: Vec<Skill>,
215}
216
217fn default_session_agent_kind() -> String {
218 "agent".to_owned()
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
223#[non_exhaustive]
224pub struct SessionStats {
225 pub duration_seconds: f64,
227 pub active_seconds: f64,
229}
230
231#[derive(Debug, Clone, Serialize)]
235#[non_exhaustive]
236pub struct CreateSessionRequest {
237 pub agent: AgentRef,
240 pub environment_id: String,
242 #[serde(skip_serializing_if = "Vec::is_empty")]
244 pub vault_ids: Vec<String>,
245 #[serde(skip_serializing_if = "Vec::is_empty")]
249 pub resources: Vec<SessionResource>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub title: Option<String>,
253}
254
255impl CreateSessionRequest {
256 #[must_use]
258 pub fn builder() -> CreateSessionRequestBuilder {
259 CreateSessionRequestBuilder::default()
260 }
261}
262
263#[derive(Debug, Default)]
265pub struct CreateSessionRequestBuilder {
266 agent: Option<AgentRef>,
267 environment_id: Option<String>,
268 vault_ids: Vec<String>,
269 resources: Vec<SessionResource>,
270 title: Option<String>,
271}
272
273impl CreateSessionRequestBuilder {
274 #[must_use]
276 pub fn agent(mut self, agent: impl Into<AgentRef>) -> Self {
277 self.agent = Some(agent.into());
278 self
279 }
280
281 #[must_use]
283 pub fn environment_id(mut self, id: impl Into<String>) -> Self {
284 self.environment_id = Some(id.into());
285 self
286 }
287
288 #[must_use]
290 pub fn vault_id(mut self, id: impl Into<String>) -> Self {
291 self.vault_ids.push(id.into());
292 self
293 }
294
295 #[must_use]
297 pub fn vault_ids(mut self, ids: Vec<String>) -> Self {
298 self.vault_ids = ids;
299 self
300 }
301
302 #[must_use]
306 pub fn resource(mut self, resource: SessionResource) -> Self {
307 self.resources.push(resource);
308 self
309 }
310
311 #[must_use]
313 pub fn title(mut self, title: impl Into<String>) -> Self {
314 self.title = Some(title.into());
315 self
316 }
317
318 pub fn build(self) -> Result<CreateSessionRequest> {
325 let agent = self
326 .agent
327 .ok_or_else(|| crate::Error::InvalidConfig("agent is required".into()))?;
328 let environment_id = self
329 .environment_id
330 .ok_or_else(|| crate::Error::InvalidConfig("environment_id is required".into()))?;
331 Ok(CreateSessionRequest {
332 agent,
333 environment_id,
334 vault_ids: self.vault_ids,
335 resources: self.resources,
336 title: self.title,
337 })
338 }
339}
340
341#[derive(Debug, Clone, Default)]
343#[non_exhaustive]
344pub struct ListSessionsParams {
345 pub after: Option<String>,
347 pub before: Option<String>,
349 pub limit: Option<u32>,
351 pub include_archived: Option<bool>,
353}
354
355impl ListSessionsParams {
356 fn to_query(&self) -> Vec<(&'static str, String)> {
357 let mut q = Vec::new();
358 if let Some(a) = &self.after {
359 q.push(("after", a.clone()));
360 }
361 if let Some(b) = &self.before {
362 q.push(("before", b.clone()));
363 }
364 if let Some(l) = self.limit {
365 q.push(("limit", l.to_string()));
366 }
367 if let Some(ia) = self.include_archived {
368 q.push(("include_archived", ia.to_string()));
369 }
370 q
371 }
372}
373
374pub struct Sessions<'a> {
386 client: &'a Client,
387 research_preview: bool,
388}
389
390#[derive(Debug, Clone, Default, Serialize)]
396#[non_exhaustive]
397pub struct UpdateSessionRequest {
398 #[serde(skip_serializing_if = "Option::is_none")]
401 pub title: Option<String>,
402 #[serde(skip_serializing_if = "Option::is_none")]
405 pub metadata: Option<super::agents::MetadataPatch>,
406 #[serde(skip_serializing_if = "Vec::is_empty")]
409 pub vault_ids: Vec<String>,
410}
411
412impl UpdateSessionRequest {
413 #[must_use]
415 pub fn new() -> Self {
416 Self::default()
417 }
418
419 #[must_use]
421 pub fn title(mut self, title: impl Into<String>) -> Self {
422 self.title = Some(title.into());
423 self
424 }
425
426 #[must_use]
428 pub fn metadata(mut self, patch: super::agents::MetadataPatch) -> Self {
429 self.metadata = Some(patch);
430 self
431 }
432}
433
434impl<'a> Sessions<'a> {
435 pub(crate) fn new(client: &'a Client) -> Self {
436 Self {
437 client,
438 research_preview: false,
439 }
440 }
441
442 #[must_use]
468 pub fn with_research_preview(mut self) -> Self {
469 self.research_preview = true;
470 self
471 }
472
473 pub async fn create(&self, request: CreateSessionRequest) -> Result<Session> {
475 let request_ref = &request;
476 self.client
477 .execute_with_retry(
478 || {
479 self.client
480 .request_builder(reqwest::Method::POST, "/v1/sessions")
481 .json(request_ref)
482 },
483 betas(self.research_preview),
484 )
485 .await
486 }
487
488 pub async fn retrieve(&self, session_id: &str) -> Result<Session> {
490 let path = format!("/v1/sessions/{session_id}");
491 self.client
492 .execute_with_retry(
493 || self.client.request_builder(reqwest::Method::GET, &path),
494 betas(self.research_preview),
495 )
496 .await
497 }
498
499 pub async fn list(&self, params: ListSessionsParams) -> Result<Paginated<Session>> {
501 let query = params.to_query();
502 self.client
503 .execute_with_retry(
504 || {
505 let mut req = self
506 .client
507 .request_builder(reqwest::Method::GET, "/v1/sessions");
508 for (k, v) in &query {
509 req = req.query(&[(k, v)]);
510 }
511 req
512 },
513 betas(self.research_preview),
514 )
515 .await
516 }
517
518 pub async fn update(&self, session_id: &str, request: UpdateSessionRequest) -> Result<Session> {
522 let path = format!("/v1/sessions/{session_id}");
523 let request_ref = &request;
524 self.client
525 .execute_with_retry(
526 || {
527 self.client
528 .request_builder(reqwest::Method::POST, &path)
529 .json(request_ref)
530 },
531 betas(self.research_preview),
532 )
533 .await
534 }
535
536 pub async fn archive(&self, session_id: &str) -> Result<Session> {
539 let path = format!("/v1/sessions/{session_id}/archive");
540 self.client
541 .execute_with_retry(
542 || self.client.request_builder(reqwest::Method::POST, &path),
543 betas(self.research_preview),
544 )
545 .await
546 }
547
548 #[must_use]
552 pub fn events(&self, session_id: impl Into<String>) -> super::events::Events<'_> {
553 super::events::Events {
554 client: self.client,
555 session_id: session_id.into(),
556 research_preview: self.research_preview,
557 }
558 }
559
560 #[must_use]
565 pub fn resources(&self, session_id: impl Into<String>) -> super::resources::Resources<'_> {
566 super::resources::Resources {
567 client: self.client,
568 session_id: session_id.into(),
569 }
570 }
571
572 #[must_use]
576 pub fn threads(&self, session_id: impl Into<String>) -> super::threads::Threads<'_> {
577 super::threads::Threads {
578 client: self.client,
579 session_id: session_id.into(),
580 research_preview: self.research_preview,
581 }
582 }
583
584 pub async fn delete(&self, session_id: &str) -> Result<()> {
589 let path = format!("/v1/sessions/{session_id}");
590 let _: serde_json::Value = self
594 .client
595 .execute_with_retry(
596 || self.client.request_builder(reqwest::Method::DELETE, &path),
597 betas(self.research_preview),
598 )
599 .await?;
600 Ok(())
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use pretty_assertions::assert_eq;
608 use serde_json::json;
609 use wiremock::matchers::{body_partial_json, header, method, path};
610 use wiremock::{Mock, MockServer, ResponseTemplate};
611
612 fn client_for(mock: &MockServer) -> Client {
613 Client::builder()
614 .api_key("sk-ant-test")
615 .base_url(mock.uri())
616 .build()
617 .unwrap()
618 }
619
620 fn fake_session(id: &str) -> serde_json::Value {
621 json!({
622 "id": id,
623 "status": "idle",
624 "title": "Test session",
625 "usage": {
626 "input_tokens": 0,
627 "output_tokens": 0,
628 "cache_creation_input_tokens": 0,
629 "cache_read_input_tokens": 0
630 },
631 "created_at": "2026-04-30T12:00:00Z"
632 })
633 }
634
635 #[test]
636 fn agent_ref_serializes_string_form_untagged() {
637 let r = AgentRef::latest("agent_01ABC");
638 let v = serde_json::to_value(&r).unwrap();
639 assert_eq!(v, json!("agent_01ABC"));
640 }
641
642 #[test]
643 fn agent_ref_serializes_pinned_form_with_type_tag() {
644 let r = AgentRef::pinned("agent_01ABC", 3);
645 let v = serde_json::to_value(&r).unwrap();
646 assert_eq!(
647 v,
648 json!({"type": "agent", "id": "agent_01ABC", "version": 3})
649 );
650 }
651
652 #[test]
653 fn agent_ref_round_trips_both_forms() {
654 for r in [AgentRef::latest("a"), AgentRef::pinned("a", 1)] {
655 let v = serde_json::to_value(&r).unwrap();
656 let parsed: AgentRef = serde_json::from_value(v).unwrap();
657 assert_eq!(parsed, r);
658 }
659 }
660
661 #[test]
662 fn create_session_request_drops_empty_optional_fields() {
663 let req = CreateSessionRequest::builder()
664 .agent("agent_01")
665 .environment_id("env_01")
666 .build()
667 .unwrap();
668 let v = serde_json::to_value(&req).unwrap();
669 assert!(v.get("vault_ids").is_none(), "{v}");
670 assert!(v.get("resources").is_none(), "{v}");
671 assert!(v.get("title").is_none(), "{v}");
672 }
673
674 #[tokio::test]
675 async fn create_posts_to_v1_sessions_with_beta_header() {
676 let mock = MockServer::start().await;
677 Mock::given(method("POST"))
678 .and(path("/v1/sessions"))
679 .and(header("anthropic-beta", "managed-agents-2026-04-01"))
680 .and(body_partial_json(json!({
681 "agent": "agent_01",
682 "environment_id": "env_01"
683 })))
684 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_01")))
685 .mount(&mock)
686 .await;
687
688 let client = client_for(&mock);
689 let req = CreateSessionRequest::builder()
690 .agent("agent_01")
691 .environment_id("env_01")
692 .build()
693 .unwrap();
694 let s = client
695 .managed_agents()
696 .sessions()
697 .create(req)
698 .await
699 .unwrap();
700 assert_eq!(s.id, "sesn_01");
701 assert_eq!(s.status, SessionStatus::Idle);
702 assert_eq!(s.title.as_deref(), Some("Test session"));
703 }
704
705 #[tokio::test]
706 async fn create_with_pinned_agent_serializes_object_form() {
707 let mock = MockServer::start().await;
708 Mock::given(method("POST"))
709 .and(path("/v1/sessions"))
710 .and(body_partial_json(json!({
711 "agent": {"type": "agent", "id": "agent_01", "version": 2}
712 })))
713 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_01")))
714 .mount(&mock)
715 .await;
716
717 let client = client_for(&mock);
718 let req = CreateSessionRequest::builder()
719 .agent(AgentRef::pinned("agent_01", 2))
720 .environment_id("env_01")
721 .build()
722 .unwrap();
723 let _ = client
724 .managed_agents()
725 .sessions()
726 .create(req)
727 .await
728 .unwrap();
729 }
730
731 #[tokio::test]
732 async fn create_with_vault_ids_includes_them_in_body() {
733 let mock = MockServer::start().await;
734 Mock::given(method("POST"))
735 .and(path("/v1/sessions"))
736 .and(body_partial_json(
737 json!({"vault_ids": ["vault_01", "vault_02"]}),
738 ))
739 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_01")))
740 .mount(&mock)
741 .await;
742
743 let client = client_for(&mock);
744 let req = CreateSessionRequest::builder()
745 .agent("agent_01")
746 .environment_id("env_01")
747 .vault_id("vault_01")
748 .vault_id("vault_02")
749 .build()
750 .unwrap();
751 let _ = client
752 .managed_agents()
753 .sessions()
754 .create(req)
755 .await
756 .unwrap();
757 }
758
759 #[tokio::test]
760 async fn retrieve_returns_typed_session() {
761 let mock = MockServer::start().await;
762 Mock::given(method("GET"))
763 .and(path("/v1/sessions/sesn_42"))
764 .and(header("anthropic-beta", "managed-agents-2026-04-01"))
765 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
766 "id": "sesn_42",
767 "status": "running"
768 })))
769 .mount(&mock)
770 .await;
771
772 let client = client_for(&mock);
773 let s = client
774 .managed_agents()
775 .sessions()
776 .retrieve("sesn_42")
777 .await
778 .unwrap();
779 assert_eq!(s.id, "sesn_42");
780 assert_eq!(s.status, SessionStatus::Running);
781 }
782
783 #[tokio::test]
784 async fn list_passes_pagination_query_params() {
785 let mock = MockServer::start().await;
786 Mock::given(method("GET"))
787 .and(path("/v1/sessions"))
788 .and(wiremock::matchers::query_param("limit", "5"))
789 .and(wiremock::matchers::query_param("include_archived", "true"))
790 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
791 "data": [
792 {"id": "sesn_a", "status": "idle"},
793 {"id": "sesn_b", "status": "terminated"}
794 ],
795 "has_more": false
796 })))
797 .mount(&mock)
798 .await;
799
800 let client = client_for(&mock);
801 let page = client
802 .managed_agents()
803 .sessions()
804 .list(ListSessionsParams {
805 limit: Some(5),
806 include_archived: Some(true),
807 ..Default::default()
808 })
809 .await
810 .unwrap();
811 assert_eq!(page.data.len(), 2);
812 }
813
814 #[tokio::test]
815 async fn archive_posts_to_archive_subpath() {
816 let mock = MockServer::start().await;
817 Mock::given(method("POST"))
818 .and(path("/v1/sessions/sesn_x/archive"))
819 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
820 "id": "sesn_x",
821 "status": "idle",
822 "archived_at": "2026-04-30T12:00:00Z"
823 })))
824 .mount(&mock)
825 .await;
826
827 let client = client_for(&mock);
828 let s = client
829 .managed_agents()
830 .sessions()
831 .archive("sesn_x")
832 .await
833 .unwrap();
834 assert_eq!(s.archived_at.as_deref(), Some("2026-04-30T12:00:00Z"));
835 }
836
837 #[tokio::test]
838 async fn delete_returns_unit_on_success() {
839 let mock = MockServer::start().await;
840 Mock::given(method("DELETE"))
841 .and(path("/v1/sessions/sesn_x"))
842 .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
843 .mount(&mock)
844 .await;
845
846 let client = client_for(&mock);
847 client
848 .managed_agents()
849 .sessions()
850 .delete("sesn_x")
851 .await
852 .unwrap();
853 }
854
855 #[tokio::test]
856 async fn update_posts_to_session_path_with_merge_patch_body() {
857 let mock = MockServer::start().await;
858 Mock::given(method("POST"))
859 .and(path("/v1/sessions/sesn_u"))
860 .and(body_partial_json(json!({
861 "title": "renamed",
862 "metadata": {"plan": "pro", "old": null}
863 })))
864 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_u")))
865 .mount(&mock)
866 .await;
867
868 let client = client_for(&mock);
869 let s = client
870 .managed_agents()
871 .sessions()
872 .update(
873 "sesn_u",
874 UpdateSessionRequest::new().title("renamed").metadata(
875 super::super::agents::MetadataPatch::new()
876 .set("plan", "pro")
877 .delete("old"),
878 ),
879 )
880 .await
881 .unwrap();
882 assert_eq!(s.id, "sesn_u");
883 }
884
885 #[test]
886 fn session_decodes_full_response_with_agent_snapshot_environment_and_stats() {
887 let raw = json!({
889 "id": "sesn_full",
890 "type": "session",
891 "status": "idle",
892 "agent": {
893 "type": "agent",
894 "id": "agent_X",
895 "version": 3,
896 "name": "Lead",
897 "description": "An agent",
898 "model": "claude-sonnet-4-6",
899 "system": "you are an agent",
900 "tools": [],
901 "mcp_servers": [],
902 "skills": []
903 },
904 "environment_id": "env_Y",
905 "vault_ids": ["vlt_a", "vlt_b"],
906 "title": "demo",
907 "metadata": {"team": "research"},
908 "stats": {"duration_seconds": 123.5, "active_seconds": 45.0},
909 "resources": [],
910 "created_at": "2026-04-30T12:00:00Z",
911 "updated_at": "2026-04-30T12:01:00Z"
912 });
913 let s: Session = serde_json::from_value(raw).unwrap();
914 assert_eq!(s.kind, "session");
915 let agent = s.agent.unwrap();
916 assert_eq!(agent.id, "agent_X");
917 assert_eq!(agent.version, 3);
918 assert_eq!(s.environment_id.as_deref(), Some("env_Y"));
919 assert_eq!(s.vault_ids, vec!["vlt_a", "vlt_b"]);
920 assert_eq!(s.metadata.get("team").map(String::as_str), Some("research"));
921 let stats = s.stats.unwrap();
922 assert!((stats.duration_seconds - 123.5).abs() < 1e-6);
923 assert!((stats.active_seconds - 45.0).abs() < 1e-6);
924 }
925
926 #[tokio::test]
927 async fn retrieve_without_research_preview_sends_only_base_beta() {
928 let mock = MockServer::start().await;
929 Mock::given(method("GET"))
930 .and(path("/v1/sessions/sesn_x"))
931 .and(header("anthropic-beta", "managed-agents-2026-04-01"))
932 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_x")))
933 .mount(&mock)
934 .await;
935 let client = client_for(&mock);
936 let _ = client
937 .managed_agents()
938 .sessions()
939 .retrieve("sesn_x")
940 .await
941 .unwrap();
942 }
943
944 #[tokio::test]
945 async fn retrieve_with_research_preview_sends_both_beta_headers() {
946 let mock = MockServer::start().await;
947 Mock::given(method("GET"))
948 .and(path("/v1/sessions/sesn_x"))
949 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_x")))
950 .mount(&mock)
951 .await;
952 let client = client_for(&mock);
953 let _ = client
954 .managed_agents()
955 .sessions()
956 .with_research_preview()
957 .retrieve("sesn_x")
958 .await
959 .unwrap();
960 let received = &mock.received_requests().await.unwrap()[0];
961 let beta = received
962 .headers
963 .get("anthropic-beta")
964 .unwrap()
965 .to_str()
966 .unwrap();
967 assert!(
968 beta.contains("managed-agents-2026-04-01")
969 && beta.contains("managed-agents-2026-04-01-research-preview"),
970 "expected both beta values, got {beta}"
971 );
972 }
973
974 #[tokio::test]
975 async fn events_sub_handle_inherits_research_preview_flag() {
976 let mock = MockServer::start().await;
977 Mock::given(method("POST"))
978 .and(path("/v1/sessions/sesn_x/events"))
979 .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
980 .mount(&mock)
981 .await;
982 let client = client_for(&mock);
983 client
984 .managed_agents()
985 .sessions()
986 .with_research_preview()
987 .events("sesn_x")
988 .send(&[super::super::events::OutgoingUserEvent::message("ping")])
989 .await
990 .unwrap();
991 let received = &mock.received_requests().await.unwrap()[0];
992 let beta = received
993 .headers
994 .get("anthropic-beta")
995 .unwrap()
996 .to_str()
997 .unwrap();
998 assert!(
999 beta.contains("managed-agents-2026-04-01-research-preview"),
1000 "events sub-handle did not inherit research_preview flag (beta={beta})"
1001 );
1002 }
1003
1004 #[tokio::test]
1005 async fn threads_sub_handle_inherits_research_preview_flag() {
1006 let mock = MockServer::start().await;
1007 Mock::given(method("GET"))
1008 .and(path("/v1/sessions/sesn_x/threads"))
1009 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1010 "data": [],
1011 "has_more": false
1012 })))
1013 .mount(&mock)
1014 .await;
1015 let client = client_for(&mock);
1016 let _ = client
1017 .managed_agents()
1018 .sessions()
1019 .with_research_preview()
1020 .threads("sesn_x")
1021 .list()
1022 .await
1023 .unwrap();
1024 let received = &mock.received_requests().await.unwrap()[0];
1025 let beta = received
1026 .headers
1027 .get("anthropic-beta")
1028 .unwrap()
1029 .to_str()
1030 .unwrap();
1031 assert!(
1032 beta.contains("managed-agents-2026-04-01-research-preview"),
1033 "threads sub-handle did not inherit research_preview flag (beta={beta})"
1034 );
1035 }
1036}