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::MANAGED_AGENTS_BETA;
17use super::agents::{AgentMcpServer, AgentModel, AgentTool, Skill};
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> {
378 client: &'a Client,
379}
380
381#[derive(Debug, Clone, Default, Serialize)]
387#[non_exhaustive]
388pub struct UpdateSessionRequest {
389 #[serde(skip_serializing_if = "Option::is_none")]
392 pub title: Option<String>,
393 #[serde(skip_serializing_if = "Option::is_none")]
396 pub metadata: Option<super::agents::MetadataPatch>,
397 #[serde(skip_serializing_if = "Vec::is_empty")]
400 pub vault_ids: Vec<String>,
401}
402
403impl UpdateSessionRequest {
404 #[must_use]
406 pub fn new() -> Self {
407 Self::default()
408 }
409
410 #[must_use]
412 pub fn title(mut self, title: impl Into<String>) -> Self {
413 self.title = Some(title.into());
414 self
415 }
416
417 #[must_use]
419 pub fn metadata(mut self, patch: super::agents::MetadataPatch) -> Self {
420 self.metadata = Some(patch);
421 self
422 }
423}
424
425impl<'a> Sessions<'a> {
426 pub(crate) fn new(client: &'a Client) -> Self {
427 Self { client }
428 }
429
430 pub async fn create(&self, request: CreateSessionRequest) -> Result<Session> {
432 let request_ref = &request;
433 self.client
434 .execute_with_retry(
435 || {
436 self.client
437 .request_builder(reqwest::Method::POST, "/v1/sessions")
438 .json(request_ref)
439 },
440 &[MANAGED_AGENTS_BETA],
441 )
442 .await
443 }
444
445 pub async fn retrieve(&self, session_id: &str) -> Result<Session> {
447 let path = format!("/v1/sessions/{session_id}");
448 self.client
449 .execute_with_retry(
450 || self.client.request_builder(reqwest::Method::GET, &path),
451 &[MANAGED_AGENTS_BETA],
452 )
453 .await
454 }
455
456 pub async fn list(&self, params: ListSessionsParams) -> Result<Paginated<Session>> {
458 let query = params.to_query();
459 self.client
460 .execute_with_retry(
461 || {
462 let mut req = self
463 .client
464 .request_builder(reqwest::Method::GET, "/v1/sessions");
465 for (k, v) in &query {
466 req = req.query(&[(k, v)]);
467 }
468 req
469 },
470 &[MANAGED_AGENTS_BETA],
471 )
472 .await
473 }
474
475 pub async fn update(&self, session_id: &str, request: UpdateSessionRequest) -> Result<Session> {
479 let path = format!("/v1/sessions/{session_id}");
480 let request_ref = &request;
481 self.client
482 .execute_with_retry(
483 || {
484 self.client
485 .request_builder(reqwest::Method::POST, &path)
486 .json(request_ref)
487 },
488 &[MANAGED_AGENTS_BETA],
489 )
490 .await
491 }
492
493 pub async fn archive(&self, session_id: &str) -> Result<Session> {
496 let path = format!("/v1/sessions/{session_id}/archive");
497 self.client
498 .execute_with_retry(
499 || self.client.request_builder(reqwest::Method::POST, &path),
500 &[MANAGED_AGENTS_BETA],
501 )
502 .await
503 }
504
505 #[must_use]
508 pub fn events(&self, session_id: impl Into<String>) -> super::events::Events<'_> {
509 super::events::Events {
510 client: self.client,
511 session_id: session_id.into(),
512 }
513 }
514
515 #[must_use]
518 pub fn resources(&self, session_id: impl Into<String>) -> super::resources::Resources<'_> {
519 super::resources::Resources {
520 client: self.client,
521 session_id: session_id.into(),
522 }
523 }
524
525 #[must_use]
529 pub fn threads(&self, session_id: impl Into<String>) -> super::threads::Threads<'_> {
530 super::threads::Threads {
531 client: self.client,
532 session_id: session_id.into(),
533 }
534 }
535
536 pub async fn delete(&self, session_id: &str) -> Result<()> {
541 let path = format!("/v1/sessions/{session_id}");
542 let _: serde_json::Value = self
546 .client
547 .execute_with_retry(
548 || self.client.request_builder(reqwest::Method::DELETE, &path),
549 &[MANAGED_AGENTS_BETA],
550 )
551 .await?;
552 Ok(())
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use pretty_assertions::assert_eq;
560 use serde_json::json;
561 use wiremock::matchers::{body_partial_json, header, method, path};
562 use wiremock::{Mock, MockServer, ResponseTemplate};
563
564 fn client_for(mock: &MockServer) -> Client {
565 Client::builder()
566 .api_key("sk-ant-test")
567 .base_url(mock.uri())
568 .build()
569 .unwrap()
570 }
571
572 fn fake_session(id: &str) -> serde_json::Value {
573 json!({
574 "id": id,
575 "status": "idle",
576 "title": "Test session",
577 "usage": {
578 "input_tokens": 0,
579 "output_tokens": 0,
580 "cache_creation_input_tokens": 0,
581 "cache_read_input_tokens": 0
582 },
583 "created_at": "2026-04-30T12:00:00Z"
584 })
585 }
586
587 #[test]
588 fn agent_ref_serializes_string_form_untagged() {
589 let r = AgentRef::latest("agent_01ABC");
590 let v = serde_json::to_value(&r).unwrap();
591 assert_eq!(v, json!("agent_01ABC"));
592 }
593
594 #[test]
595 fn agent_ref_serializes_pinned_form_with_type_tag() {
596 let r = AgentRef::pinned("agent_01ABC", 3);
597 let v = serde_json::to_value(&r).unwrap();
598 assert_eq!(
599 v,
600 json!({"type": "agent", "id": "agent_01ABC", "version": 3})
601 );
602 }
603
604 #[test]
605 fn agent_ref_round_trips_both_forms() {
606 for r in [AgentRef::latest("a"), AgentRef::pinned("a", 1)] {
607 let v = serde_json::to_value(&r).unwrap();
608 let parsed: AgentRef = serde_json::from_value(v).unwrap();
609 assert_eq!(parsed, r);
610 }
611 }
612
613 #[test]
614 fn create_session_request_drops_empty_optional_fields() {
615 let req = CreateSessionRequest::builder()
616 .agent("agent_01")
617 .environment_id("env_01")
618 .build()
619 .unwrap();
620 let v = serde_json::to_value(&req).unwrap();
621 assert!(v.get("vault_ids").is_none(), "{v}");
622 assert!(v.get("resources").is_none(), "{v}");
623 assert!(v.get("title").is_none(), "{v}");
624 }
625
626 #[tokio::test]
627 async fn create_posts_to_v1_sessions_with_beta_header() {
628 let mock = MockServer::start().await;
629 Mock::given(method("POST"))
630 .and(path("/v1/sessions"))
631 .and(header("anthropic-beta", "managed-agents-2026-04-01"))
632 .and(body_partial_json(json!({
633 "agent": "agent_01",
634 "environment_id": "env_01"
635 })))
636 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_01")))
637 .mount(&mock)
638 .await;
639
640 let client = client_for(&mock);
641 let req = CreateSessionRequest::builder()
642 .agent("agent_01")
643 .environment_id("env_01")
644 .build()
645 .unwrap();
646 let s = client
647 .managed_agents()
648 .sessions()
649 .create(req)
650 .await
651 .unwrap();
652 assert_eq!(s.id, "sesn_01");
653 assert_eq!(s.status, SessionStatus::Idle);
654 assert_eq!(s.title.as_deref(), Some("Test session"));
655 }
656
657 #[tokio::test]
658 async fn create_with_pinned_agent_serializes_object_form() {
659 let mock = MockServer::start().await;
660 Mock::given(method("POST"))
661 .and(path("/v1/sessions"))
662 .and(body_partial_json(json!({
663 "agent": {"type": "agent", "id": "agent_01", "version": 2}
664 })))
665 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_01")))
666 .mount(&mock)
667 .await;
668
669 let client = client_for(&mock);
670 let req = CreateSessionRequest::builder()
671 .agent(AgentRef::pinned("agent_01", 2))
672 .environment_id("env_01")
673 .build()
674 .unwrap();
675 let _ = client
676 .managed_agents()
677 .sessions()
678 .create(req)
679 .await
680 .unwrap();
681 }
682
683 #[tokio::test]
684 async fn create_with_vault_ids_includes_them_in_body() {
685 let mock = MockServer::start().await;
686 Mock::given(method("POST"))
687 .and(path("/v1/sessions"))
688 .and(body_partial_json(
689 json!({"vault_ids": ["vault_01", "vault_02"]}),
690 ))
691 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_01")))
692 .mount(&mock)
693 .await;
694
695 let client = client_for(&mock);
696 let req = CreateSessionRequest::builder()
697 .agent("agent_01")
698 .environment_id("env_01")
699 .vault_id("vault_01")
700 .vault_id("vault_02")
701 .build()
702 .unwrap();
703 let _ = client
704 .managed_agents()
705 .sessions()
706 .create(req)
707 .await
708 .unwrap();
709 }
710
711 #[tokio::test]
712 async fn retrieve_returns_typed_session() {
713 let mock = MockServer::start().await;
714 Mock::given(method("GET"))
715 .and(path("/v1/sessions/sesn_42"))
716 .and(header("anthropic-beta", "managed-agents-2026-04-01"))
717 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
718 "id": "sesn_42",
719 "status": "running"
720 })))
721 .mount(&mock)
722 .await;
723
724 let client = client_for(&mock);
725 let s = client
726 .managed_agents()
727 .sessions()
728 .retrieve("sesn_42")
729 .await
730 .unwrap();
731 assert_eq!(s.id, "sesn_42");
732 assert_eq!(s.status, SessionStatus::Running);
733 }
734
735 #[tokio::test]
736 async fn list_passes_pagination_query_params() {
737 let mock = MockServer::start().await;
738 Mock::given(method("GET"))
739 .and(path("/v1/sessions"))
740 .and(wiremock::matchers::query_param("limit", "5"))
741 .and(wiremock::matchers::query_param("include_archived", "true"))
742 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
743 "data": [
744 {"id": "sesn_a", "status": "idle"},
745 {"id": "sesn_b", "status": "terminated"}
746 ],
747 "has_more": false
748 })))
749 .mount(&mock)
750 .await;
751
752 let client = client_for(&mock);
753 let page = client
754 .managed_agents()
755 .sessions()
756 .list(ListSessionsParams {
757 limit: Some(5),
758 include_archived: Some(true),
759 ..Default::default()
760 })
761 .await
762 .unwrap();
763 assert_eq!(page.data.len(), 2);
764 }
765
766 #[tokio::test]
767 async fn archive_posts_to_archive_subpath() {
768 let mock = MockServer::start().await;
769 Mock::given(method("POST"))
770 .and(path("/v1/sessions/sesn_x/archive"))
771 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
772 "id": "sesn_x",
773 "status": "idle",
774 "archived_at": "2026-04-30T12:00:00Z"
775 })))
776 .mount(&mock)
777 .await;
778
779 let client = client_for(&mock);
780 let s = client
781 .managed_agents()
782 .sessions()
783 .archive("sesn_x")
784 .await
785 .unwrap();
786 assert_eq!(s.archived_at.as_deref(), Some("2026-04-30T12:00:00Z"));
787 }
788
789 #[tokio::test]
790 async fn delete_returns_unit_on_success() {
791 let mock = MockServer::start().await;
792 Mock::given(method("DELETE"))
793 .and(path("/v1/sessions/sesn_x"))
794 .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
795 .mount(&mock)
796 .await;
797
798 let client = client_for(&mock);
799 client
800 .managed_agents()
801 .sessions()
802 .delete("sesn_x")
803 .await
804 .unwrap();
805 }
806
807 #[tokio::test]
808 async fn update_posts_to_session_path_with_merge_patch_body() {
809 let mock = MockServer::start().await;
810 Mock::given(method("POST"))
811 .and(path("/v1/sessions/sesn_u"))
812 .and(body_partial_json(json!({
813 "title": "renamed",
814 "metadata": {"plan": "pro", "old": null}
815 })))
816 .respond_with(ResponseTemplate::new(200).set_body_json(fake_session("sesn_u")))
817 .mount(&mock)
818 .await;
819
820 let client = client_for(&mock);
821 let s = client
822 .managed_agents()
823 .sessions()
824 .update(
825 "sesn_u",
826 UpdateSessionRequest::new().title("renamed").metadata(
827 super::super::agents::MetadataPatch::new()
828 .set("plan", "pro")
829 .delete("old"),
830 ),
831 )
832 .await
833 .unwrap();
834 assert_eq!(s.id, "sesn_u");
835 }
836
837 #[test]
838 fn session_decodes_full_response_with_agent_snapshot_environment_and_stats() {
839 let raw = json!({
841 "id": "sesn_full",
842 "type": "session",
843 "status": "idle",
844 "agent": {
845 "type": "agent",
846 "id": "agent_X",
847 "version": 3,
848 "name": "Lead",
849 "description": "An agent",
850 "model": "claude-sonnet-4-6",
851 "system": "you are an agent",
852 "tools": [],
853 "mcp_servers": [],
854 "skills": []
855 },
856 "environment_id": "env_Y",
857 "vault_ids": ["vlt_a", "vlt_b"],
858 "title": "demo",
859 "metadata": {"team": "research"},
860 "stats": {"duration_seconds": 123.5, "active_seconds": 45.0},
861 "resources": [],
862 "created_at": "2026-04-30T12:00:00Z",
863 "updated_at": "2026-04-30T12:01:00Z"
864 });
865 let s: Session = serde_json::from_value(raw).unwrap();
866 assert_eq!(s.kind, "session");
867 let agent = s.agent.unwrap();
868 assert_eq!(agent.id, "agent_X");
869 assert_eq!(agent.version, 3);
870 assert_eq!(s.environment_id.as_deref(), Some("env_Y"));
871 assert_eq!(s.vault_ids, vec!["vlt_a", "vlt_b"]);
872 assert_eq!(s.metadata.get("team").map(String::as_str), Some("research"));
873 let stats = s.stats.unwrap();
874 assert!((stats.duration_seconds - 123.5).abs() < 1e-6);
875 assert!((stats.active_seconds - 45.0).abs() < 1e-6);
876 }
877}