use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use futures::stream::Stream;
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use super::dreams::{CreateDreamParams, Dream, DreamListResponse};
use super::events::{SessionEvent, UserEvent};
use super::memory::{
CreateMemoryParams, CreateMemoryStoreParams, Memory, MemoryListResponse, MemoryStore,
MemoryVersion, UpdateMemoryParams,
};
use super::stream::process_managed_agents_sse;
use super::types::{
Agent, CreateAgentParams, CreateEnvironmentParams, CreateSessionParams, Environment,
ListResponse, ListSessionsParams, Session, SessionResourceResponse, SessionThread,
};
use super::vaults::{
CreateCredentialParams, CreateVaultParams, Credential, CredentialValidation,
UpdateCredentialParams, Vault, VaultListResponse,
};
use crate::base_url::validate_base_url;
use crate::{Error, Result};
const DEFAULT_BASE_URL: &str = "https://api.anthropic.com";
const DEFAULT_SSE_TIMEOUT_SECS: u64 = 300;
#[derive(Debug, Clone)]
pub struct ManagedAgentsClient {
pub(crate) client: reqwest::Client,
#[allow(dead_code)] pub(crate) api_key: String,
pub(crate) base_url: String,
pub(crate) sse_timeout: Duration,
pub(crate) cached_headers: Arc<HeaderMap>,
}
impl ManagedAgentsClient {
pub fn new(api_key: impl Into<String>) -> Result<Self> {
let api_key = api_key.into();
let cached_headers = Arc::new(build_headers(&api_key)?);
Ok(Self {
client: reqwest::Client::new(),
api_key,
base_url: DEFAULT_BASE_URL.to_string(),
sse_timeout: Duration::from_secs(DEFAULT_SSE_TIMEOUT_SECS),
cached_headers,
})
}
pub fn from_env() -> Result<Self> {
let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| Error::Authentication {
message: "ANTHROPIC_API_KEY environment variable is not set".to_string(),
})?;
Self::new(api_key)
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Result<Self> {
let base_url = base_url.into();
validate_base_url(&base_url)?;
self.base_url = base_url;
Ok(self)
}
pub fn with_sse_timeout(mut self, timeout: Duration) -> Self {
self.sse_timeout = timeout;
self
}
pub(crate) fn build_url(&self, endpoint: &str) -> String {
let base = self.base_url.trim_end_matches('/');
format!("{base}/v1/{endpoint}")
}
pub async fn create_environment(&self, params: CreateEnvironmentParams) -> Result<Environment> {
let url = self.build_url("environments");
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| {
Error::connection(format!("failed to send create_environment request: {e}"), None)
})?;
handle_response(response).await
}
pub async fn get_environment(&self, environment_id: &str) -> Result<Environment> {
let url = self.build_url(&format!("environments/{environment_id}"));
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to send get_environment request: {e}"), None),
)?;
handle_response(response).await
}
pub async fn delete_environment(&self, environment_id: &str) -> Result<()> {
let url = self.build_url(&format!("environments/{environment_id}"));
let response = self
.client
.delete(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| {
Error::connection(format!("failed to send delete_environment request: {e}"), None)
})?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn create_agent(&self, params: CreateAgentParams) -> Result<Agent> {
let url = self.build_url("agents");
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| {
Error::connection(format!("failed to send create_agent request: {e}"), None)
})?;
handle_response(response).await
}
pub async fn list_agents(&self) -> Result<Vec<Agent>> {
let url = self.build_url("agents");
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to send list_agents request: {e}"), None),
)?;
let list: ListResponse<Agent> = handle_response(response).await?;
Ok(list.data)
}
pub async fn get_agent(&self, agent_id: &str) -> Result<Agent> {
let url = self.build_url(&format!("agents/{agent_id}"));
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to send get_agent request: {e}"), None),
)?;
handle_response(response).await
}
pub async fn delete_agent(&self, agent_id: &str) -> Result<()> {
let url = self.build_url(&format!("agents/{agent_id}"));
let response =
self.client.delete(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to send delete_agent request: {e}"), None),
)?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn create_session(&self, params: CreateSessionParams) -> Result<Session> {
let url = self.build_url("sessions");
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| {
Error::connection(format!("failed to send create_session request: {e}"), None)
})?;
handle_response(response).await
}
pub async fn get_session(&self, session_id: &str) -> Result<Session> {
let url = self.build_url(&format!("sessions/{session_id}"));
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to send get_session request: {e}"), None),
)?;
handle_response(response).await
}
pub async fn list_sessions(&self, params: Option<ListSessionsParams>) -> Result<Vec<Session>> {
let url = self.build_url("sessions");
let mut request = self.client.get(&url).headers((*self.cached_headers).clone());
if let Some(params) = ¶ms {
if let Some(agent_id) = ¶ms.agent_id {
request = request.query(&[("agent_id", agent_id.as_str())]);
}
if let Some(limit) = params.limit {
request = request.query(&[("limit", &limit.to_string())]);
}
}
let response = request.send().await.map_err(|e| {
Error::connection(format!("failed to send list_sessions request: {e}"), None)
})?;
let list: ListResponse<Session> = handle_response(response).await?;
Ok(list.data)
}
pub async fn archive_session(&self, session_id: &str) -> Result<()> {
let url = self.build_url(&format!("sessions/{session_id}/archive"));
let response =
self.client.post(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to send archive_session request: {e}"), None),
)?;
handle_empty_response(response).await
}
pub async fn delete_session(&self, session_id: &str) -> Result<()> {
let url = self.build_url(&format!("sessions/{session_id}"));
let response =
self.client.delete(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to send delete_session request: {e}"), None),
)?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn send_event(&self, session_id: &str, event: UserEvent) -> Result<()> {
use super::events::SendEventsRequest;
let url = format!("{}?beta=true", self.build_url(&format!("sessions/{session_id}/events")));
let body = SendEventsRequest { events: vec![event] };
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(&body)
.send()
.await
.map_err(|e| {
Error::connection(format!("failed to send send_event request: {e}"), None)
})?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn stream_events(
&self,
session_id: &str,
) -> Result<Pin<Box<dyn Stream<Item = Result<SessionEvent>> + Send>>> {
let url = format!(
"{}?beta=true",
self.build_url(&format!("sessions/{session_id}/events/stream"))
);
let mut headers = (*self.cached_headers).clone();
headers.insert(reqwest::header::ACCEPT, HeaderValue::from_static("text/event-stream"));
let response = self
.client
.get(&url)
.headers(headers)
.send()
.await
.map_err(|e| Error::connection(format!("failed to open SSE stream: {e}"), None))?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(map_api_error(status, &body));
}
let byte_stream = response.bytes_stream();
Ok(process_managed_agents_sse(byte_stream, self.sse_timeout))
}
}
impl ManagedAgentsClient {
pub async fn interrupt(&self, session_id: &str) -> Result<()> {
self.send_event(session_id, UserEvent::Interrupt {}).await
}
pub async fn custom_tool_result(
&self,
session_id: &str,
custom_tool_use_id: &str,
content: impl Into<String>,
) -> Result<()> {
let event = UserEvent::custom_tool_result(custom_tool_use_id, content);
self.send_event(session_id, event).await
}
pub async fn allow_tool(&self, session_id: &str, tool_use_id: &str) -> Result<()> {
let event = UserEvent::allow_tool(tool_use_id);
self.send_event(session_id, event).await
}
pub async fn deny_tool(
&self,
session_id: &str,
tool_use_id: &str,
reason: impl Into<String>,
) -> Result<()> {
let event = UserEvent::deny_tool(tool_use_id, reason);
self.send_event(session_id, event).await
}
pub async fn define_outcome(
&self,
session_id: &str,
criteria: impl Into<String>,
) -> Result<()> {
let event = UserEvent::DefineOutcome { criteria: criteria.into() };
self.send_event(session_id, event).await
}
pub async fn archive_agent(&self, agent_id: &str) -> Result<()> {
let url = self.build_url(&format!("agents/{agent_id}/archive"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to archive agent: {e}"), None))?;
handle_empty_response(response).await
}
pub async fn archive_environment(&self, environment_id: &str) -> Result<()> {
let url = self.build_url(&format!("environments/{environment_id}/archive"));
let response =
self.client.post(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to archive environment: {e}"), None),
)?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn create_vault(&self, params: CreateVaultParams) -> Result<Vault> {
let url = self.build_url("vaults");
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| Error::connection(format!("failed to create vault: {e}"), None))?;
handle_response(response).await
}
pub async fn list_vaults(&self) -> Result<Vec<Vault>> {
let url = self.build_url("vaults");
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to list vaults: {e}"), None))?;
let list: VaultListResponse<Vault> = handle_response(response).await?;
Ok(list.data)
}
pub async fn get_vault(&self, vault_id: &str) -> Result<Vault> {
let url = self.build_url(&format!("vaults/{vault_id}"));
let response = self
.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to get vault: {e}"), None))?;
handle_response(response).await
}
pub async fn archive_vault(&self, vault_id: &str) -> Result<()> {
let url = self.build_url(&format!("vaults/{vault_id}/archive"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to archive vault: {e}"), None))?;
handle_empty_response(response).await
}
pub async fn delete_vault(&self, vault_id: &str) -> Result<()> {
let url = self.build_url(&format!("vaults/{vault_id}"));
let response = self
.client
.delete(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to delete vault: {e}"), None))?;
handle_empty_response(response).await
}
pub async fn create_credential(
&self,
vault_id: &str,
params: CreateCredentialParams,
) -> Result<Credential> {
let url = self.build_url(&format!("vaults/{vault_id}/credentials"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| Error::connection(format!("failed to create credential: {e}"), None))?;
handle_response(response).await
}
pub async fn list_credentials(&self, vault_id: &str) -> Result<Vec<Credential>> {
let url = self.build_url(&format!("vaults/{vault_id}/credentials"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to list credentials: {e}"), None))?;
let list: VaultListResponse<Credential> = handle_response(response).await?;
Ok(list.data)
}
pub async fn get_credential(&self, vault_id: &str, credential_id: &str) -> Result<Credential> {
let url = self.build_url(&format!("vaults/{vault_id}/credentials/{credential_id}"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to get credential: {e}"), None))?;
handle_response(response).await
}
pub async fn update_credential(
&self,
vault_id: &str,
credential_id: &str,
params: UpdateCredentialParams,
) -> Result<Credential> {
let url = self.build_url(&format!("vaults/{vault_id}/credentials/{credential_id}"));
let response = self
.client
.patch(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| Error::connection(format!("failed to update credential: {e}"), None))?;
handle_response(response).await
}
pub async fn archive_credential(&self, vault_id: &str, credential_id: &str) -> Result<()> {
let url = self.build_url(&format!("vaults/{vault_id}/credentials/{credential_id}/archive"));
let response =
self.client.post(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to archive credential: {e}"), None),
)?;
handle_empty_response(response).await
}
pub async fn delete_credential(&self, vault_id: &str, credential_id: &str) -> Result<()> {
let url = self.build_url(&format!("vaults/{vault_id}/credentials/{credential_id}"));
let response =
self.client.delete(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to delete credential: {e}"), None),
)?;
handle_empty_response(response).await
}
pub async fn validate_credential(
&self,
vault_id: &str,
credential_id: &str,
) -> Result<CredentialValidation> {
let url = format!(
"{}?beta=true",
self.build_url(&format!(
"vaults/{vault_id}/credentials/{credential_id}/mcp_oauth_validate"
))
);
let response =
self.client.post(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to validate credential: {e}"), None),
)?;
handle_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn create_memory_store(
&self,
params: CreateMemoryStoreParams,
) -> Result<MemoryStore> {
let url = self.build_url("memory_stores");
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| Error::connection(format!("failed to create memory store: {e}"), None))?;
handle_response(response).await
}
pub async fn list_memory_stores(&self) -> Result<Vec<MemoryStore>> {
let url = self.build_url("memory_stores");
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to list memory stores: {e}"), None),
)?;
let list: MemoryListResponse<MemoryStore> = handle_response(response).await?;
Ok(list.data)
}
pub async fn get_memory_store(&self, store_id: &str) -> Result<MemoryStore> {
let url = self.build_url(&format!("memory_stores/{store_id}"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to get memory store: {e}"), None))?;
handle_response(response).await
}
pub async fn archive_memory_store(&self, store_id: &str) -> Result<()> {
let url = self.build_url(&format!("memory_stores/{store_id}/archive"));
let response =
self.client.post(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to archive memory store: {e}"), None),
)?;
handle_empty_response(response).await
}
pub async fn delete_memory_store(&self, store_id: &str) -> Result<()> {
let url = self.build_url(&format!("memory_stores/{store_id}"));
let response =
self.client.delete(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to delete memory store: {e}"), None),
)?;
handle_empty_response(response).await
}
pub async fn create_memory(
&self,
store_id: &str,
params: CreateMemoryParams,
) -> Result<Memory> {
let url = self.build_url(&format!("memory_stores/{store_id}/memories"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| Error::connection(format!("failed to create memory: {e}"), None))?;
handle_response(response).await
}
pub async fn list_memories(&self, store_id: &str) -> Result<Vec<Memory>> {
let url = self.build_url(&format!("memory_stores/{store_id}/memories"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to list memories: {e}"), None))?;
let list: MemoryListResponse<Memory> = handle_response(response).await?;
Ok(list.data)
}
pub async fn get_memory(&self, store_id: &str, memory_id: &str) -> Result<Memory> {
let url = self.build_url(&format!("memory_stores/{store_id}/memories/{memory_id}"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to get memory: {e}"), None))?;
handle_response(response).await
}
pub async fn update_memory(
&self,
store_id: &str,
memory_id: &str,
params: UpdateMemoryParams,
) -> Result<Memory> {
let url = self.build_url(&format!("memory_stores/{store_id}/memories/{memory_id}"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(¶ms)
.send()
.await
.map_err(|e| Error::connection(format!("failed to update memory: {e}"), None))?;
handle_response(response).await
}
pub async fn delete_memory(&self, store_id: &str, memory_id: &str) -> Result<()> {
let url = self.build_url(&format!("memory_stores/{store_id}/memories/{memory_id}"));
let response = self
.client
.delete(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to delete memory: {e}"), None))?;
handle_empty_response(response).await
}
pub async fn list_memory_versions(&self, store_id: &str) -> Result<Vec<MemoryVersion>> {
let url = self.build_url(&format!("memory_stores/{store_id}/memory_versions"));
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to list memory versions: {e}"), None),
)?;
let list: MemoryListResponse<MemoryVersion> = handle_response(response).await?;
Ok(list.data)
}
pub async fn get_memory_version(
&self,
store_id: &str,
version_id: &str,
) -> Result<MemoryVersion> {
let url = self.build_url(&format!("memory_stores/{store_id}/memory_versions/{version_id}"));
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to get memory version: {e}"), None),
)?;
handle_response(response).await
}
pub async fn redact_memory_version(&self, store_id: &str, version_id: &str) -> Result<()> {
let url = self
.build_url(&format!("memory_stores/{store_id}/memory_versions/{version_id}/redact"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(&serde_json::json!({}))
.send()
.await
.map_err(|e| {
Error::connection(format!("failed to redact memory version: {e}"), None)
})?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn list_threads(&self, session_id: &str) -> Result<Vec<SessionThread>> {
let url = self.build_url(&format!("sessions/{session_id}/threads"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to list threads: {e}"), None))?;
let list: ListResponse<SessionThread> = handle_response(response).await?;
Ok(list.data)
}
pub async fn stream_thread_events(
&self,
session_id: &str,
thread_id: &str,
) -> Result<Pin<Box<dyn Stream<Item = Result<SessionEvent>> + Send>>> {
let url = format!(
"{}?beta=true",
self.build_url(&format!("sessions/{session_id}/threads/{thread_id}/stream"))
);
let mut headers = (*self.cached_headers).clone();
headers.insert(reqwest::header::ACCEPT, HeaderValue::from_static("text/event-stream"));
let response =
self.client.get(&url).headers(headers).send().await.map_err(|e| {
Error::connection(format!("failed to open thread stream: {e}"), None)
})?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(map_api_error(status, &body));
}
let byte_stream = response.bytes_stream();
Ok(process_managed_agents_sse(byte_stream, self.sse_timeout))
}
pub async fn archive_thread(&self, session_id: &str, thread_id: &str) -> Result<()> {
let url = self.build_url(&format!("sessions/{session_id}/threads/{thread_id}/archive"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to archive thread: {e}"), None))?;
handle_empty_response(response).await
}
pub async fn interrupt_thread(&self, session_id: &str, thread_id: &str) -> Result<()> {
let url = format!("{}?beta=true", self.build_url(&format!("sessions/{session_id}/events")));
let body = serde_json::json!({
"events": [{
"type": "user.interrupt",
"session_thread_id": thread_id,
}]
});
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(&body)
.send()
.await
.map_err(|e| Error::connection(format!("failed to interrupt thread: {e}"), None))?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn get_work_stats(&self, environment_id: &str) -> Result<serde_json::Value> {
let url = self.build_url(&format!("environments/{environment_id}/work/stats"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to get work stats: {e}"), None))?;
handle_response(response).await
}
pub async fn stop_work(&self, environment_id: &str, work_id: &str, force: bool) -> Result<()> {
let url = self.build_url(&format!("environments/{environment_id}/work/{work_id}/stop"));
let body = if force { serde_json::json!({"force": true}) } else { serde_json::json!({}) };
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(&body)
.send()
.await
.map_err(|e| Error::connection(format!("failed to stop work: {e}"), None))?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn create_dream(&self, params: CreateDreamParams) -> Result<Dream> {
let url = self.build_url("dreams");
let mut headers = (*self.cached_headers).clone();
headers.insert(
"anthropic-beta",
HeaderValue::from_static("managed-agents-2026-04-01,dreaming-2026-04-21"),
);
let response = self
.client
.post(&url)
.headers(headers)
.json(¶ms)
.send()
.await
.map_err(|e| Error::connection(format!("failed to create dream: {e}"), None))?;
handle_response(response).await
}
pub async fn get_dream(&self, dream_id: &str) -> Result<Dream> {
let url = self.build_url(&format!("dreams/{dream_id}"));
let mut headers = (*self.cached_headers).clone();
headers.insert(
"anthropic-beta",
HeaderValue::from_static("managed-agents-2026-04-01,dreaming-2026-04-21"),
);
let response = self
.client
.get(&url)
.headers(headers)
.send()
.await
.map_err(|e| Error::connection(format!("failed to get dream: {e}"), None))?;
handle_response(response).await
}
pub async fn list_dreams(&self) -> Result<Vec<Dream>> {
let url = self.build_url("dreams");
let mut headers = (*self.cached_headers).clone();
headers.insert(
"anthropic-beta",
HeaderValue::from_static("managed-agents-2026-04-01,dreaming-2026-04-21"),
);
let response = self
.client
.get(&url)
.headers(headers)
.send()
.await
.map_err(|e| Error::connection(format!("failed to list dreams: {e}"), None))?;
let list: DreamListResponse = handle_response(response).await?;
Ok(list.data)
}
pub async fn cancel_dream(&self, dream_id: &str) -> Result<()> {
let url = self.build_url(&format!("dreams/{dream_id}/cancel"));
let mut headers = (*self.cached_headers).clone();
headers.insert(
"anthropic-beta",
HeaderValue::from_static("managed-agents-2026-04-01,dreaming-2026-04-21"),
);
let response = self
.client
.post(&url)
.headers(headers)
.send()
.await
.map_err(|e| Error::connection(format!("failed to cancel dream: {e}"), None))?;
handle_empty_response(response).await
}
pub async fn archive_dream(&self, dream_id: &str) -> Result<()> {
let url = self.build_url(&format!("dreams/{dream_id}/archive"));
let mut headers = (*self.cached_headers).clone();
headers.insert(
"anthropic-beta",
HeaderValue::from_static("managed-agents-2026-04-01,dreaming-2026-04-21"),
);
let response = self
.client
.post(&url)
.headers(headers)
.send()
.await
.map_err(|e| Error::connection(format!("failed to archive dream: {e}"), None))?;
handle_empty_response(response).await
}
}
impl ManagedAgentsClient {
pub async fn upload_file(
&self,
filename: impl Into<String>,
data: Vec<u8>,
) -> Result<serde_json::Value> {
let url = self.build_url("files");
let filename = filename.into();
let mime = infer_mime(&filename);
let part =
reqwest::multipart::Part::bytes(data).file_name(filename).mime_str(mime).map_err(
|e| Error::BadRequest { message: format!("invalid mime type: {e}"), param: None },
)?;
let form = reqwest::multipart::Form::new().part("file", part);
let mut headers = HeaderMap::new();
headers.insert("x-api-key", self.cached_headers.get("x-api-key").unwrap().clone());
headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
headers.insert("anthropic-beta", HeaderValue::from_static("managed-agents-2026-04-01"));
let response = self
.client
.post(&url)
.headers(headers)
.multipart(form)
.send()
.await
.map_err(|e| Error::connection(format!("failed to upload file: {e}"), None))?;
handle_response(response).await
}
pub async fn download_file(&self, file_id: &str) -> Result<Vec<u8>> {
let url = self.build_url(&format!("files/{file_id}/content"));
let response =
self.client
.get(&url)
.headers((*self.cached_headers).clone())
.send()
.await
.map_err(|e| Error::connection(format!("failed to download file: {e}"), None))?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(map_api_error(status, &body));
}
response.bytes().await.map(|b| b.to_vec()).map_err(|e| Error::Connection {
message: format!("failed to read file content: {e}"),
source: None,
})
}
pub async fn list_session_files(&self, session_id: &str) -> Result<Vec<serde_json::Value>> {
let url = format!("{}?scope_id={session_id}", self.build_url("files"));
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to list session files: {e}"), None),
)?;
let body: serde_json::Value = handle_response(response).await?;
Ok(body.get("data").and_then(|d| d.as_array()).cloned().unwrap_or_default())
}
}
fn infer_mime(filename: &str) -> &'static str {
let ext = filename.rsplit('.').next().unwrap_or("").to_lowercase();
match ext.as_str() {
"pdf" => "application/pdf",
"txt" | "text" | "md" => "text/plain",
"csv" => "text/csv",
"json" => "application/json",
"jpg" | "jpeg" => "image/jpeg",
"png" => "image/png",
"gif" => "image/gif",
"webp" => "image/webp",
_ => "application/octet-stream",
}
}
impl ManagedAgentsClient {
pub async fn add_session_resource(
&self,
session_id: &str,
resource: serde_json::Value,
) -> Result<SessionResourceResponse> {
let url = self.build_url(&format!("sessions/{session_id}/resources"));
let response = self
.client
.post(&url)
.headers((*self.cached_headers).clone())
.json(&resource)
.send()
.await
.map_err(|e| Error::connection(format!("failed to add session resource: {e}"), None))?;
handle_response(response).await
}
pub async fn list_session_resources(
&self,
session_id: &str,
) -> Result<Vec<SessionResourceResponse>> {
let url = self.build_url(&format!("sessions/{session_id}/resources"));
let response =
self.client.get(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to list session resources: {e}"), None),
)?;
let list: ListResponse<SessionResourceResponse> = handle_response(response).await?;
Ok(list.data)
}
pub async fn delete_session_resource(&self, session_id: &str, resource_id: &str) -> Result<()> {
let url = self.build_url(&format!("sessions/{session_id}/resources/{resource_id}"));
let response =
self.client.delete(&url).headers((*self.cached_headers).clone()).send().await.map_err(
|e| Error::connection(format!("failed to delete session resource: {e}"), None),
)?;
handle_empty_response(response).await
}
}
async fn handle_response<T: serde::de::DeserializeOwned>(response: reqwest::Response) -> Result<T> {
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(map_api_error(status, &body));
}
let body = response.text().await.map_err(|e| Error::Serialization {
message: format!("failed to read response body: {e}"),
source: None,
})?;
serde_json::from_str::<T>(&body).map_err(|e| Error::Serialization {
message: format!("failed to deserialize response: {e}\nBody: {body}"),
source: None,
})
}
async fn handle_empty_response(response: reqwest::Response) -> Result<()> {
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(map_api_error(status, &body));
}
Ok(())
}
fn map_api_error(status: reqwest::StatusCode, body: &str) -> Error {
let (error_type, message) = parse_error_body(body);
match status.as_u16() {
400 => Error::BadRequest { message, param: None },
401 => Error::Authentication { message },
403 => Error::Permission { message },
404 => Error::NotFound { message, resource_type: None, resource_id: None },
408 => Error::Timeout { message, duration: None },
429 => Error::RateLimit { message, retry_after: None },
500 => Error::InternalServer { message, request_id: None },
502..=504 => Error::ServiceUnavailable { message, retry_after: None },
_ => Error::Api {
status_code: status.as_u16(),
error_type: Some(error_type),
message,
request_id: None,
},
}
}
fn parse_error_body(body: &str) -> (String, String) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(body) {
let error_obj = json.get("error").unwrap_or(&json);
let error_type =
error_obj.get("type").and_then(|v| v.as_str()).unwrap_or("api_error").to_string();
let message = error_obj.get("message").and_then(|v| v.as_str()).unwrap_or(body).to_string();
(error_type, message)
} else {
("api_error".to_string(), body.to_string())
}
}
fn build_headers(api_key: &str) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
headers.insert(
"x-api-key",
HeaderValue::from_str(api_key).map_err(|e| Error::Authentication {
message: format!("invalid API key header value: {e}"),
})?,
);
headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
headers.insert("anthropic-beta", HeaderValue::from_static("managed-agents-2026-04-01"));
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
Ok(headers)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_creates_client_with_defaults() {
let client = ManagedAgentsClient::new("test-api-key").unwrap();
assert_eq!(client.base_url, "https://api.anthropic.com");
assert_eq!(client.sse_timeout, Duration::from_secs(300));
assert_eq!(client.api_key, "test-api-key");
}
#[test]
fn test_with_base_url_overrides_default() {
let client = ManagedAgentsClient::new("test-api-key")
.unwrap()
.with_base_url("https://custom.example.com")
.unwrap();
assert_eq!(client.base_url, "https://custom.example.com");
}
#[test]
fn test_with_base_url_rejects_cleartext_http() {
let err = ManagedAgentsClient::new("test-api-key")
.unwrap()
.with_base_url("http://managed-agents.internal.example.com")
.expect_err("a non-loopback http base URL must be rejected");
assert!(err.is_validation(), "expected a validation error, got {err}");
let message = err.to_string();
assert!(
message.contains("unencrypted"),
"error should explain the cleartext risk, got: {message}"
);
assert!(message.contains("https://"), "error should suggest https, got: {message}");
}
#[test]
fn test_with_base_url_allows_loopback_http_for_local_dev() {
for url in ["http://localhost:8080", "http://127.0.0.1:8080", "http://[::1]:8080"] {
let client = ManagedAgentsClient::new("test-api-key")
.unwrap()
.with_base_url(url)
.unwrap_or_else(|e| panic!("loopback url {url} should be accepted: {e}"));
assert_eq!(client.base_url, url);
}
}
#[test]
fn test_with_base_url_rejects_non_http_schemes_and_garbage() {
for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
let err = ManagedAgentsClient::new("test-api-key")
.unwrap()
.with_base_url(url)
.expect_err("non-https, non-loopback base URL must be rejected");
assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
}
}
#[test]
fn test_default_base_url_is_https() {
let client = ManagedAgentsClient::new("test-api-key").unwrap();
assert!(client.base_url.starts_with("https://"));
assert!(validate_base_url(&client.base_url).is_ok());
}
#[test]
fn test_with_sse_timeout_overrides_default() {
let client = ManagedAgentsClient::new("test-api-key")
.unwrap()
.with_sse_timeout(Duration::from_secs(600));
assert_eq!(client.sse_timeout, Duration::from_secs(600));
}
#[test]
fn test_build_url_constructs_correct_path() {
let client = ManagedAgentsClient::new("test-api-key").unwrap();
assert_eq!(client.build_url("agents"), "https://api.anthropic.com/v1/agents");
assert_eq!(
client.build_url("sessions/sess_123/events"),
"https://api.anthropic.com/v1/sessions/sess_123/events"
);
}
#[test]
fn test_build_url_trims_trailing_slash() {
let client = ManagedAgentsClient::new("test-api-key")
.unwrap()
.with_base_url("https://api.anthropic.com/")
.unwrap();
assert_eq!(client.build_url("agents"), "https://api.anthropic.com/v1/agents");
}
#[test]
fn test_build_headers_includes_required_headers() {
let headers = build_headers("test-key").unwrap();
assert_eq!(headers.get("x-api-key").unwrap(), "test-key");
assert_eq!(headers.get("anthropic-version").unwrap(), "2023-06-01");
assert_eq!(headers.get("anthropic-beta").unwrap(), "managed-agents-2026-04-01");
assert_eq!(headers.get("content-type").unwrap(), "application/json");
}
#[test]
fn test_from_env_missing_key_returns_authentication_error() {
let original = std::env::var("ANTHROPIC_API_KEY").ok();
unsafe {
std::env::remove_var("ANTHROPIC_API_KEY");
}
let result = ManagedAgentsClient::from_env();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_authentication());
if let Some(val) = original {
unsafe {
std::env::set_var("ANTHROPIC_API_KEY", val);
}
}
}
}