Skip to main content

claude_api/managed_agents/
sessions.rs

1//! Sessions: provision, retrieve, list, archive, delete.
2//!
3//! A session is a running agent instance within an environment. Each
4//! session references an [agent](super::agents) (by ID or pinned to a
5//! version) and an environment, and maintains conversation history
6//! across multiple interactions.
7
8use 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/// Session lifecycle status.
21///
22/// Sessions start in [`Idle`](Self::Idle), transition to
23/// [`Running`](Self::Running) while processing, and may briefly
24/// [`Rescheduling`](Self::Rescheduling) on transient retries.
25/// [`Terminated`](Self::Terminated) is terminal.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28#[non_exhaustive]
29pub enum SessionStatus {
30    /// Agent is waiting for input. Sessions start in this state.
31    Idle,
32    /// Agent is actively executing.
33    Running,
34    /// Transient error occurred; session is retrying automatically.
35    Rescheduling,
36    /// Session has ended due to an unrecoverable error.
37    Terminated,
38}
39
40/// Reference to an agent: either a string ID (latest version) or a
41/// pinned `{type, id, version}` object.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(untagged)]
44#[non_exhaustive]
45pub enum AgentRef {
46    /// Bare ID; resolves to the latest published version of the agent.
47    Latest(String),
48    /// Pinned to a specific version.
49    Pinned {
50        /// Always `"agent"`.
51        #[serde(rename = "type")]
52        ty: String,
53        /// Agent ID.
54        id: String,
55        /// Agent version number.
56        version: u32,
57    },
58}
59
60impl AgentRef {
61    /// Build an [`AgentRef::Latest`].
62    #[must_use]
63    pub fn latest(id: impl Into<String>) -> Self {
64        Self::Latest(id.into())
65    }
66
67    /// Build an [`AgentRef::Pinned`] for a specific version.
68    #[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/// Cumulative token usage on a session.
91#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
92#[non_exhaustive]
93pub struct SessionUsage {
94    /// Uncached input tokens billed across all model calls.
95    #[serde(default)]
96    pub input_tokens: u64,
97    /// Total output tokens across all model calls.
98    #[serde(default)]
99    pub output_tokens: u64,
100    /// Tokens written to the prompt cache, broken down by cache TTL.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub cache_creation: Option<CacheCreationUsage>,
103    /// Tokens served from the prompt cache (cheaper read path).
104    #[serde(default)]
105    pub cache_read_input_tokens: u64,
106}
107
108/// Prompt-cache write tokens, split by ephemeral TTL.
109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
110#[non_exhaustive]
111pub struct CacheCreationUsage {
112    /// Tokens used to create 5-minute ephemeral cache entries.
113    #[serde(default)]
114    pub ephemeral_5m_input_tokens: u64,
115    /// Tokens used to create 1-hour ephemeral cache entries.
116    #[serde(default)]
117    pub ephemeral_1h_input_tokens: u64,
118}
119
120/// A Managed Agents session.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[non_exhaustive]
123pub struct Session {
124    /// Stable session identifier (`sesn_...`).
125    pub id: String,
126    /// Wire `type`; always `"session"`.
127    #[serde(rename = "type", default = "default_session_kind")]
128    pub kind: String,
129    /// Lifecycle status.
130    pub status: SessionStatus,
131    /// Resolved snapshot of the agent at session-creation time. Pinned
132    /// even if the underlying agent is later updated.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub agent: Option<SessionAgent>,
135    /// ID of the environment this session runs in.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub environment_id: Option<String>,
138    /// Vault references for MCP credential resolution.
139    #[serde(default, skip_serializing_if = "Vec::is_empty")]
140    pub vault_ids: Vec<String>,
141    /// Optional human-readable title.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub title: Option<String>,
144    /// Free-form key-value metadata attached at create time.
145    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
146    pub metadata: HashMap<String, String>,
147    /// Cumulative token usage. May be absent on freshly-created sessions.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub usage: Option<SessionUsage>,
150    /// Wall-clock and active-runtime stats.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub stats: Option<SessionStats>,
153    /// Mounted resources: file uploads, GitHub repositories, memory
154    /// stores. Each carries a server-assigned `id` (`sesrsc_...`) used
155    /// for [`Resources::update`](super::resources::Resources::update)
156    /// and [`Resources::delete`](super::resources::Resources::delete).
157    #[serde(default, skip_serializing_if = "Vec::is_empty")]
158    pub resources: Vec<SessionResource>,
159    /// Outcome evaluations recorded against this session, when an
160    /// outcome was defined. **Research-preview**: this field is
161    /// populated only when the session uses the outcomes feature
162    /// (`user.define_outcome` events, requires
163    /// `managed-agents-2026-04-01-research-preview` beta header).
164    /// Preserved as `Vec<Value>` until the outcomes types land.
165    #[serde(default, skip_serializing_if = "Vec::is_empty")]
166    pub outcome_evaluations: Vec<serde_json::Value>,
167    /// Timestamp when the session was created (RFC3339 / ISO 8601).
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub created_at: Option<String>,
170    /// Timestamp of the most recent update.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub updated_at: Option<String>,
173    /// Set when the session has been archived.
174    #[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/// Snapshot of an agent's configuration at the moment a session was
183/// created. Mirrors [`Agent`](super::agents::Agent) but is pinned --
184/// later edits to the agent don't change a session's recorded
185/// snapshot.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[non_exhaustive]
188pub struct SessionAgent {
189    /// Wire `type`; always `"agent"`.
190    #[serde(rename = "type", default = "default_session_agent_kind")]
191    pub kind: String,
192    /// Agent ID (`agnt_...`).
193    pub id: String,
194    /// Pinned agent version.
195    pub version: u32,
196    /// Agent name as it was at snapshot time.
197    pub name: String,
198    /// Agent description. May be `null` if no description was set.
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub description: Option<String>,
201    /// Model configuration.
202    pub model: AgentModel,
203    /// System prompt. May be `null` if no system prompt was set.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub system: Option<String>,
206    /// Tools available to the agent at snapshot time.
207    #[serde(default, skip_serializing_if = "Vec::is_empty")]
208    pub tools: Vec<AgentTool>,
209    /// MCP servers configured.
210    #[serde(default, skip_serializing_if = "Vec::is_empty")]
211    pub mcp_servers: Vec<AgentMcpServer>,
212    /// Skills loaded into the container.
213    #[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/// Wall-clock and active-runtime statistics for a session.
222#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
223#[non_exhaustive]
224pub struct SessionStats {
225    /// Total elapsed seconds since session creation.
226    pub duration_seconds: f64,
227    /// Seconds the agent was actively executing (excluding idle time).
228    pub active_seconds: f64,
229}
230
231/// Request body for `POST /v1/sessions`.
232///
233/// Build via [`CreateSessionRequest::builder`].
234#[derive(Debug, Clone, Serialize)]
235#[non_exhaustive]
236pub struct CreateSessionRequest {
237    /// The agent driving this session. May be a bare ID string (latest
238    /// version) or a pinned [`AgentRef::Pinned`].
239    pub agent: AgentRef,
240    /// Environment the session runs in.
241    pub environment_id: String,
242    /// Optional vault references for MCP credential resolution.
243    #[serde(skip_serializing_if = "Vec::is_empty")]
244    pub vault_ids: Vec<String>,
245    /// Optional resources mounted into the session container at
246    /// creation time. Build with the typed constructors in
247    /// [`crate::managed_agents::resources`].
248    #[serde(skip_serializing_if = "Vec::is_empty")]
249    pub resources: Vec<SessionResource>,
250    /// Optional human-readable title.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub title: Option<String>,
253}
254
255impl CreateSessionRequest {
256    /// Begin configuring a request.
257    #[must_use]
258    pub fn builder() -> CreateSessionRequestBuilder {
259        CreateSessionRequestBuilder::default()
260    }
261}
262
263/// Builder for [`CreateSessionRequest`].
264#[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    /// Set the agent. Required.
275    #[must_use]
276    pub fn agent(mut self, agent: impl Into<AgentRef>) -> Self {
277        self.agent = Some(agent.into());
278        self
279    }
280
281    /// Set the environment. Required.
282    #[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    /// Append a vault ID for credential resolution.
289    #[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    /// Set the full vault list.
296    #[must_use]
297    pub fn vault_ids(mut self, ids: Vec<String>) -> Self {
298        self.vault_ids = ids;
299        self
300    }
301
302    /// Append a typed resource (file / `github_repository` /
303    /// `memory_store`). Build via the constructors in
304    /// [`crate::managed_agents::resources`].
305    #[must_use]
306    pub fn resource(mut self, resource: SessionResource) -> Self {
307        self.resources.push(resource);
308        self
309    }
310
311    /// Set a human-readable title.
312    #[must_use]
313    pub fn title(mut self, title: impl Into<String>) -> Self {
314        self.title = Some(title.into());
315        self
316    }
317
318    /// Finalize.
319    ///
320    /// # Errors
321    ///
322    /// Returns [`Error::InvalidConfig`](crate::Error::InvalidConfig)
323    /// if `agent` or `environment_id` was not set.
324    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/// Optional knobs for [`Sessions::list`].
342#[derive(Debug, Clone, Default)]
343#[non_exhaustive]
344pub struct ListSessionsParams {
345    /// Pagination cursor: results after this session ID.
346    pub after: Option<String>,
347    /// Pagination cursor: results before this session ID.
348    pub before: Option<String>,
349    /// Page size limit.
350    pub limit: Option<u32>,
351    /// Whether to include archived sessions.
352    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
374/// Namespace handle for the Sessions API.
375///
376/// Obtained via [`Client::managed_agents`](Client::managed_agents).
377pub struct Sessions<'a> {
378    client: &'a Client,
379}
380
381/// Request body for [`Sessions::update`]. All fields optional with
382/// merge-patch semantics: omit a field to preserve.
383///
384/// `metadata` is per-key: provide a `Some(value)` to upsert, or `None`
385/// to delete the key. Unspecified keys are preserved.
386#[derive(Debug, Clone, Default, Serialize)]
387#[non_exhaustive]
388pub struct UpdateSessionRequest {
389    /// New human-readable title (1-500 chars). `None` to leave
390    /// unchanged.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub title: Option<String>,
393    /// Per-key metadata patch. See
394    /// [`MetadataPatch`](super::agents::MetadataPatch).
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub metadata: Option<super::agents::MetadataPatch>,
397    /// Replace the vault attachments. **Currently rejected by the
398    /// server**; reserved for future use.
399    #[serde(skip_serializing_if = "Vec::is_empty")]
400    pub vault_ids: Vec<String>,
401}
402
403impl UpdateSessionRequest {
404    /// Empty patch.
405    #[must_use]
406    pub fn new() -> Self {
407        Self::default()
408    }
409
410    /// Set the new title.
411    #[must_use]
412    pub fn title(mut self, title: impl Into<String>) -> Self {
413        self.title = Some(title.into());
414        self
415    }
416
417    /// Apply a metadata patch.
418    #[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    /// Create a session. Returns the freshly-provisioned [`Session`].
431    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    /// Fetch a session by ID.
446    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    /// List sessions, paginated.
457    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    /// Update a session. All fields on [`UpdateSessionRequest`] are
476    /// optional with merge-patch semantics: omit a field to preserve
477    /// its current value.
478    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    /// Archive a session. Archived sessions cannot accept new events but
494    /// preserve their event history.
495    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    /// Sub-namespace for the session-events API
506    /// (`/v1/sessions/{id}/events` and the SSE stream).
507    #[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    /// Sub-namespace for resource operations on a session
516    /// (`/v1/sessions/{id}/resources`).
517    #[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    /// Sub-namespace for thread operations on a multi-agent session
526    /// (`/v1/sessions/{id}/threads`). Sub-agent threads are spawned at
527    /// runtime when the coordinator delegates to a `callable_agent`.
528    #[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    /// Delete a session permanently. The session must not be `running`;
537    /// send a `user.interrupt` event first if necessary. Files, memory
538    /// stores, environments, and agents are independent and not
539    /// affected.
540    pub async fn delete(&self, session_id: &str) -> Result<()> {
541        let path = format!("/v1/sessions/{session_id}");
542        // The delete endpoint returns 204 No Content; route through a
543        // dummy `serde_json::Value` to satisfy execute()'s
544        // DeserializeOwned bound and discard the result.
545        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        // Fixture lifted from the spec example for BetaManagedAgentsSession.
840        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}