use crate::auth::ApiKey;
use crate::error::{Error, Result};
use crate::models::*;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
use url::Url;
const DEFAULT_BASE_URL: &str = "https://custom.example.com/api";
#[derive(Clone)]
pub struct Everruns {
http: reqwest::Client,
base_url: Url,
api_key: ApiKey,
org_id: Option<HeaderValue>,
}
#[derive(Debug, Clone)]
pub struct EverrunsBuilder {
api_key: Option<ApiKey>,
base_url: String,
org_id: Option<String>,
}
impl Default for EverrunsBuilder {
fn default() -> Self {
Self {
api_key: None,
base_url: DEFAULT_BASE_URL.to_string(),
org_id: std::env::var("EVERRUNS_ORG_ID")
.ok()
.filter(|org_id| !org_id.is_empty()),
}
}
}
impl EverrunsBuilder {
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(ApiKey::new(api_key));
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn org_id(mut self, org_id: impl Into<String>) -> Self {
self.org_id = Some(org_id.into());
self
}
pub fn build(self) -> Result<Everruns> {
let api_key = match self.api_key {
Some(api_key) => api_key,
None => ApiKey::from_env()?,
};
Everruns::with_api_key_url_and_org_id(api_key, &self.base_url, self.org_id)
}
}
impl Everruns {
pub fn builder() -> EverrunsBuilder {
EverrunsBuilder::default()
}
pub fn new(api_key: impl Into<String>) -> Result<Self> {
Self::builder().api_key(api_key).build()
}
pub fn from_env() -> Result<Self> {
let base_url =
std::env::var("EVERRUNS_API_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string());
Self::builder().base_url(base_url).build()
}
pub fn with_base_url(api_key: impl Into<String>, base_url: &str) -> Result<Self> {
Self::builder().api_key(api_key).base_url(base_url).build()
}
pub fn with_org_id(api_key: impl Into<String>, org_id: impl Into<String>) -> Result<Self> {
Self::builder().api_key(api_key).org_id(org_id).build()
}
pub fn with_base_url_and_org_id(
api_key: impl Into<String>,
base_url: &str,
org_id: impl Into<String>,
) -> Result<Self> {
Self::builder()
.api_key(api_key)
.base_url(base_url)
.org_id(org_id)
.build()
}
pub fn with_api_key(api_key: ApiKey) -> Result<Self> {
Self::with_api_key_url_and_org_id(
api_key,
DEFAULT_BASE_URL,
EverrunsBuilder::default().org_id,
)
}
fn with_api_key_url_and_org_id(
api_key: ApiKey,
base_url: &str,
org_id: Option<String>,
) -> Result<Self> {
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let normalized = if base_url.ends_with('/') {
base_url.to_string()
} else {
format!("{}/", base_url)
};
let base_url = Url::parse(&normalized)?;
let org_id = org_id
.map(|org_id| {
if org_id.is_empty() {
return Err(Error::Validation("org_id cannot be empty".to_string()));
}
HeaderValue::from_str(&org_id)
.map_err(|err| Error::Validation(format!("invalid org_id header: {err}")))
})
.transpose()?;
Ok(Self {
http,
base_url,
api_key,
org_id,
})
}
pub fn agents(&self) -> AgentsClient<'_> {
AgentsClient { client: self }
}
pub fn sessions(&self) -> SessionsClient<'_> {
SessionsClient { client: self }
}
pub fn messages(&self) -> MessagesClient<'_> {
MessagesClient { client: self }
}
pub fn events(&self) -> EventsClient<'_> {
EventsClient { client: self }
}
pub fn capabilities(&self) -> CapabilitiesClient<'_> {
CapabilitiesClient { client: self }
}
pub fn workspaces(&self) -> WorkspacesClient<'_> {
WorkspacesClient { client: self }
}
pub fn workspace_files(&self) -> WorkspaceFilesClient<'_> {
WorkspaceFilesClient { client: self }
}
pub fn memories(&self) -> MemoriesClient<'_> {
MemoriesClient { client: self }
}
pub fn connections(&self) -> ConnectionsClient<'_> {
ConnectionsClient { client: self }
}
pub fn budgets(&self) -> BudgetsClient<'_> {
BudgetsClient { client: self }
}
pub(crate) fn url(&self, path: &str) -> Url {
let path_without_slash = path.strip_prefix('/').unwrap_or(path);
let full_path = format!("v1/{}", path_without_slash);
self.base_url.join(&full_path).expect("valid URL")
}
pub(crate) fn auth_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(self.api_key.expose()).expect("valid header"),
);
if let Some(org_id) = &self.org_id {
headers.insert("X-Org-Id", org_id.clone());
}
headers
}
fn headers(&self) -> HeaderMap {
let mut headers = self.auth_headers();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers
}
pub(crate) async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
let resp = self
.http
.get(self.url(path))
.headers(self.headers())
.send()
.await?;
self.handle_response(resp).await
}
pub(crate) async fn get_url<T: serde::de::DeserializeOwned>(&self, url: Url) -> Result<T> {
let resp = self.http.get(url).headers(self.headers()).send().await?;
self.handle_response(resp).await
}
pub(crate) async fn post<T: serde::de::DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let resp = self
.http
.post(self.url(path))
.headers(self.headers())
.json(body)
.send()
.await?;
self.handle_response(resp).await
}
pub(crate) async fn patch<T: serde::de::DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let resp = self
.http
.patch(self.url(path))
.headers(self.headers())
.json(body)
.send()
.await?;
self.handle_response(resp).await
}
pub(crate) async fn post_text<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &str,
) -> Result<T> {
let mut headers = self.headers();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
let resp = self
.http
.post(self.url(path))
.headers(headers)
.body(body.to_string())
.send()
.await?;
self.handle_response(resp).await
}
pub(crate) async fn post_text_url<T: serde::de::DeserializeOwned>(
&self,
url: Url,
body: &str,
) -> Result<T> {
let mut headers = self.headers();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
let resp = self
.http
.post(url)
.headers(headers)
.body(body.to_string())
.send()
.await?;
self.handle_response(resp).await
}
pub(crate) async fn get_text(&self, path: &str) -> Result<String> {
let resp = self
.http
.get(self.url(path))
.headers(self.headers())
.send()
.await?;
if resp.status().is_success() {
Ok(resp.text().await?)
} else {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
Err(Error::from_api_response(status, &body))
}
}
pub(crate) async fn put<T: serde::de::DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let resp = self
.http
.put(self.url(path))
.headers(self.headers())
.json(body)
.send()
.await?;
self.handle_response(resp).await
}
pub(crate) async fn put_empty(&self, path: &str) -> Result<()> {
let resp = self
.http
.put(self.url(path))
.headers(self.headers())
.send()
.await?;
if resp.status().is_success() {
Ok(())
} else {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
Err(Error::from_api_response(status, &body))
}
}
pub(crate) async fn delete(&self, path: &str) -> Result<()> {
let resp = self
.http
.delete(self.url(path))
.headers(self.headers())
.send()
.await?;
if resp.status().is_success() {
Ok(())
} else {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
Err(Error::from_api_response(status, &body))
}
}
pub(crate) async fn delete_url<T: serde::de::DeserializeOwned>(&self, url: Url) -> Result<T> {
let resp = self.http.delete(url).headers(self.headers()).send().await?;
self.handle_response(resp).await
}
async fn handle_response<T: serde::de::DeserializeOwned>(
&self,
resp: reqwest::Response,
) -> Result<T> {
if resp.status().is_success() {
Ok(resp.json().await?)
} else {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
Err(Error::from_api_response(status, &body))
}
}
pub(crate) fn sse_url(
&self,
session_id: &str,
since_id: Option<&str>,
types: &[&str],
exclude: &[&str],
) -> Url {
let mut url = self.url(&format!("/sessions/{}/sse", session_id));
if let Some(id) = since_id {
url.query_pairs_mut().append_pair("since_id", id);
}
for t in types {
url.query_pairs_mut().append_pair("types", t);
}
for e in exclude {
url.query_pairs_mut().append_pair("exclude", e);
}
url
}
}
pub struct AgentsClient<'a> {
client: &'a Everruns,
}
impl<'a> AgentsClient<'a> {
pub async fn list(&self) -> Result<ListResponse<Agent>> {
self.client.get("/agents").await
}
pub async fn search(&self, query: &str) -> Result<ListResponse<Agent>> {
let mut url = self.client.url("/agents");
url.query_pairs_mut().append_pair("search", query);
self.client.get_url(url).await
}
pub async fn get(&self, id: &str) -> Result<Agent> {
self.client.get(&format!("/agents/{}", id)).await
}
pub async fn stats(&self, id: &str) -> Result<ResourceStats> {
self.client.get(&format!("/agents/{}/stats", id)).await
}
pub async fn list_versions(&self, id: &str) -> Result<Vec<AgentVersion>> {
self.client.get(&format!("/agents/{}/versions", id)).await
}
pub async fn create_version(
&self,
id: &str,
req: CreateAgentVersionRequest,
) -> Result<AgentVersion> {
self.client
.post(&format!("/agents/{}/versions", id), &req)
.await
}
pub async fn set_default_version(&self, id: &str, version_id: &str) -> Result<Agent> {
let req = SetDefaultAgentVersionRequest::new(version_id);
self.client
.post(&format!("/agents/{}/versions/default", id), &req)
.await
}
pub async fn diff_versions(
&self,
id: &str,
from_version_id: &str,
to_version_id: &str,
) -> Result<AgentVersionDiffResponse> {
self.client
.get(&format!(
"/agents/{}/versions/{}/diff/{}",
id, from_version_id, to_version_id
))
.await
}
pub async fn fork_version(
&self,
id: &str,
version_id: &str,
req: ForkAgentVersionRequest,
) -> Result<Agent> {
validate_agent_name(&req.name)?;
self.client
.post(
&format!("/agents/{}/versions/{}/fork", id, version_id),
&req,
)
.await
}
pub async fn rollback_version(
&self,
id: &str,
version_id: &str,
req: RollbackAgentVersionRequest,
) -> Result<Agent> {
self.client
.post(
&format!("/agents/{}/versions/{}/rollback", id, version_id),
&req,
)
.await
}
pub async fn list_health_checks(&self, id: &str) -> Result<Vec<HealthCheckRun>> {
self.client
.get(&format!("/agents/{}/health-checks", id))
.await
}
pub async fn trigger_health_check(&self, id: &str) -> Result<HealthCheckRun> {
self.client
.post::<HealthCheckRun, _>(&format!("/agents/{}/health-checks", id), &())
.await
}
pub async fn get_health_check(&self, id: &str, run_id: &str) -> Result<HealthCheckRun> {
self.client
.get(&format!("/agents/{}/health-checks/{}", id, run_id))
.await
}
pub async fn create(&self, name: &str, system_prompt: &str) -> Result<Agent> {
validate_agent_name(name)?;
let req = CreateAgentRequest::new(name, system_prompt);
self.client.post("/agents", &req).await
}
pub async fn create_with_options(&self, req: CreateAgentRequest) -> Result<Agent> {
validate_agent_name(&req.name)?;
self.client.post("/agents", &req).await
}
pub async fn apply(&self, id: &str, name: &str, system_prompt: &str) -> Result<Agent> {
validate_agent_name(name)?;
let req = CreateAgentRequest::new(name, system_prompt).id(id);
self.client.post("/agents", &req).await
}
pub async fn apply_with_options(&self, id: &str, req: CreateAgentRequest) -> Result<Agent> {
validate_agent_name(&req.name)?;
let req = req.id(id);
self.client.post("/agents", &req).await
}
pub async fn apply_by_name(&self, name: &str, system_prompt: &str) -> Result<Agent> {
validate_agent_name(name)?;
let req = CreateAgentRequest::new(name, system_prompt);
self.client.post("/agents", &req).await
}
pub async fn apply_by_name_with_options(&self, req: CreateAgentRequest) -> Result<Agent> {
validate_agent_name(&req.name)?;
self.client.post("/agents", &req).await
}
pub async fn copy(&self, id: &str) -> Result<Agent> {
self.client
.post::<Agent, _>(&format!("/agents/{}/copy", id), &())
.await
}
pub async fn delete(&self, id: &str) -> Result<()> {
self.client.delete(&format!("/agents/{}", id)).await
}
pub async fn import(&self, content: &str) -> Result<Agent> {
self.client.post_text("/agents/import", content).await
}
pub async fn import_example(&self, example_name: &str) -> Result<Agent> {
let mut url = self.client.url("/agents/import");
url.query_pairs_mut()
.append_pair("from-example", example_name);
self.client.post_text_url(url, "").await
}
pub async fn export(&self, id: &str) -> Result<String> {
self.client
.get_text(&format!("/agents/{}/export", id))
.await
}
pub async fn analyze(&self, req: AnalyzeAgentRequest) -> Result<AgentAnalysisResponse> {
self.client.post("/agents/analyze", &req).await
}
}
pub struct SessionsClient<'a> {
client: &'a Everruns,
}
impl<'a> SessionsClient<'a> {
pub async fn list(&self) -> Result<ListResponse<Session>> {
self.client.get("/sessions").await
}
pub async fn search(&self, query: &str) -> Result<ListResponse<Session>> {
let mut url = self.client.url("/sessions");
url.query_pairs_mut().append_pair("search", query);
self.client.get_url(url).await
}
pub async fn get(&self, id: &str) -> Result<Session> {
self.client.get(&format!("/sessions/{}", id)).await
}
pub async fn create(&self) -> Result<Session> {
let req = CreateSessionRequest::new();
self.client.post("/sessions", &req).await
}
pub async fn create_with_options(&self, req: CreateSessionRequest) -> Result<Session> {
if req.harness_id.is_some() && req.harness_name.is_some() {
return Err(Error::Validation(
"Cannot specify both harness_id and harness_name".to_string(),
));
}
if let Some(ref name) = req.harness_name {
validate_harness_name(name)?;
}
self.client.post("/sessions", &req).await
}
pub async fn delete(&self, id: &str) -> Result<()> {
self.client.delete(&format!("/sessions/{}", id)).await
}
pub async fn cancel(&self, id: &str) -> Result<()> {
self.client
.post::<serde_json::Value, _>(&format!("/sessions/{}/cancel", id), &())
.await?;
Ok(())
}
pub async fn pin(&self, id: &str) -> Result<()> {
self.client
.put_empty(&format!("/sessions/{}/pin", id))
.await
}
pub async fn unpin(&self, id: &str) -> Result<()> {
self.client.delete(&format!("/sessions/{}/pin", id)).await
}
pub async fn budgets(&self, id: &str) -> Result<Vec<Budget>> {
self.client.get(&format!("/sessions/{}/budgets", id)).await
}
pub async fn budget_check(&self, id: &str) -> Result<BudgetCheckResult> {
self.client
.get(&format!("/sessions/{}/budget-check", id))
.await
}
pub async fn resume(&self, id: &str) -> Result<ResumeSessionResponse> {
self.client
.post::<ResumeSessionResponse, _>(&format!("/sessions/{}/resume", id), &())
.await
}
pub async fn set_secrets(
&self,
id: &str,
secrets: &std::collections::HashMap<String, String>,
) -> Result<()> {
let req = SetSecretsRequest::new(secrets.clone());
self.client
.put::<serde_json::Value, _>(&format!("/sessions/{}/storage/secrets", id), &req)
.await?;
Ok(())
}
pub async fn export(&self, id: &str) -> Result<String> {
self.client
.get_text(&format!("/sessions/{}/export", id))
.await
}
}
pub struct MessagesClient<'a> {
client: &'a Everruns,
}
impl<'a> MessagesClient<'a> {
pub async fn list(&self, session_id: &str) -> Result<ListResponse<Message>> {
self.client
.get(&format!("/sessions/{}/messages", session_id))
.await
}
pub async fn create(&self, session_id: &str, text: &str) -> Result<Message> {
let req = CreateMessageRequest::user_text(text);
self.client
.post(&format!("/sessions/{}/messages", session_id), &req)
.await
}
pub async fn create_tool_results(
&self,
session_id: &str,
results: Vec<ContentPart>,
) -> Result<SubmitToolResultsResponse> {
let tool_results = results
.into_iter()
.map(|part| match part {
ContentPart::ToolResult {
tool_call_id,
result,
error,
} => Ok(ClientToolResult {
tool_call_id,
result,
error,
}),
_ => Err(Error::Validation(
"create_tool_results accepts only tool_result content parts".to_string(),
)),
})
.collect::<Result<Vec<_>>>()?;
let req = SubmitToolResultsRequest { tool_results };
let path = format!("/sessions/{}/tool-results", session_id);
let mut delay = std::time::Duration::from_millis(100);
for attempt in 0..=5 {
match self.client.post(&path, &req).await {
Err(err) if attempt < 5 && is_tool_results_pending_conflict(&err) => {
tokio::time::sleep(delay).await;
delay = delay.saturating_mul(2);
}
result => return result,
}
}
unreachable!()
}
pub async fn create_with_options(
&self,
session_id: &str,
req: CreateMessageRequest,
) -> Result<Message> {
self.client
.post(&format!("/sessions/{}/messages", session_id), &req)
.await
}
}
fn is_tool_results_pending_conflict(error: &Error) -> bool {
match error {
Error::Api {
status, message, ..
} => *status == 409 && message.contains("not waiting for tool results"),
_ => false,
}
}
pub struct EventsClient<'a> {
client: &'a Everruns,
}
#[derive(Debug, Clone, Default)]
pub struct ListEventsOptions {
pub since_id: Option<String>,
pub types: Vec<String>,
pub exclude: Vec<String>,
pub limit: Option<u32>,
pub before_sequence: Option<i32>,
pub after_sequence: Option<i32>,
pub around: Option<String>,
pub window: Option<u32>,
pub from_ts: Option<String>,
pub to_ts: Option<String>,
pub turn_id: Option<String>,
pub exec_id: Option<String>,
pub trace_id: Option<String>,
pub tags: Vec<String>,
pub tool_name: Option<String>,
pub q: Option<String>,
pub order_desc: Option<bool>,
}
impl<'a> EventsClient<'a> {
pub async fn list(&self, session_id: &str) -> Result<ListResponse<Event>> {
self.client
.get(&format!("/sessions/{}/events", session_id))
.await
}
pub async fn list_with_options(
&self,
session_id: &str,
options: &ListEventsOptions,
) -> Result<ListResponse<Event>> {
let mut url = self.client.url(&format!("/sessions/{}/events", session_id));
if let Some(since_id) = &options.since_id {
url.query_pairs_mut().append_pair("since_id", since_id);
}
for t in &options.types {
url.query_pairs_mut().append_pair("types", t);
}
for e in &options.exclude {
url.query_pairs_mut().append_pair("exclude", e);
}
if let Some(limit) = options.limit {
url.query_pairs_mut()
.append_pair("limit", &limit.to_string());
}
if let Some(seq) = options.before_sequence {
url.query_pairs_mut()
.append_pair("before_sequence", &seq.to_string());
}
if let Some(seq) = options.after_sequence {
url.query_pairs_mut()
.append_pair("after_sequence", &seq.to_string());
}
if let Some(around) = &options.around {
url.query_pairs_mut().append_pair("around", around);
}
if let Some(window) = options.window {
url.query_pairs_mut()
.append_pair("window", &window.to_string());
}
if let Some(from_ts) = &options.from_ts {
url.query_pairs_mut().append_pair("from_ts", from_ts);
}
if let Some(to_ts) = &options.to_ts {
url.query_pairs_mut().append_pair("to_ts", to_ts);
}
if let Some(turn_id) = &options.turn_id {
url.query_pairs_mut().append_pair("turn_id", turn_id);
}
if let Some(exec_id) = &options.exec_id {
url.query_pairs_mut().append_pair("exec_id", exec_id);
}
if let Some(trace_id) = &options.trace_id {
url.query_pairs_mut().append_pair("trace_id", trace_id);
}
for tag in &options.tags {
url.query_pairs_mut().append_pair("tags", tag);
}
if let Some(tool_name) = &options.tool_name {
url.query_pairs_mut().append_pair("tool_name", tool_name);
}
if let Some(q) = &options.q {
url.query_pairs_mut().append_pair("q", q);
}
if let Some(order_desc) = options.order_desc {
url.query_pairs_mut()
.append_pair("order_desc", &order_desc.to_string());
}
self.client.get_url(url).await
}
pub fn stream(&self, session_id: &str) -> crate::sse::EventStream {
crate::sse::EventStream::new(
self.client.clone(),
session_id.to_string(),
crate::sse::StreamOptions::default(),
)
}
pub fn stream_with_options(
&self,
session_id: &str,
options: crate::sse::StreamOptions,
) -> crate::sse::EventStream {
crate::sse::EventStream::new(self.client.clone(), session_id.to_string(), options)
}
}
pub struct CapabilitiesClient<'a> {
client: &'a Everruns,
}
#[derive(Debug, Clone, Default)]
pub struct ListCapabilitiesOptions {
pub search: Option<String>,
pub offset: Option<u32>,
pub limit: Option<u32>,
}
impl<'a> CapabilitiesClient<'a> {
pub async fn list(&self) -> Result<ListResponse<CapabilityInfo>> {
self.client.get("/capabilities").await
}
pub async fn list_with_options(
&self,
options: &ListCapabilitiesOptions,
) -> Result<ListResponse<CapabilityInfo>> {
let mut url = self.client.url("/capabilities");
if let Some(ref search) = options.search {
url.query_pairs_mut().append_pair("search", search);
}
if let Some(offset) = options.offset {
url.query_pairs_mut()
.append_pair("offset", &offset.to_string());
}
if let Some(limit) = options.limit {
url.query_pairs_mut()
.append_pair("limit", &limit.to_string());
}
self.client.get_url(url).await
}
pub async fn get(&self, id: &str) -> Result<CapabilityInfo> {
self.client.get(&format!("/capabilities/{}", id)).await
}
pub async fn list_guardrail_examples(&self) -> Result<GuardrailExamplesResponse> {
self.client.get("/capabilities/guardrails/examples").await
}
pub async fn dry_run_guardrails(
&self,
req: GuardrailsDryRunRequest,
) -> Result<GuardrailsDryRunResponse> {
self.client
.post("/capabilities/guardrails/dry-run", &req)
.await
}
}
pub struct WorkspacesClient<'a> {
client: &'a Everruns,
}
impl<'a> WorkspacesClient<'a> {
pub async fn list(&self) -> Result<ListResponse<Workspace>> {
self.client.get("/workspaces").await
}
pub async fn list_with_options(
&self,
search: Option<&str>,
include_archived: Option<bool>,
) -> Result<ListResponse<Workspace>> {
let mut url = self.client.url("/workspaces");
if let Some(search) = search {
url.query_pairs_mut().append_pair("search", search);
}
if let Some(include_archived) = include_archived {
url.query_pairs_mut()
.append_pair("include_archived", &include_archived.to_string());
}
self.client.get_url(url).await
}
pub async fn create(&self, req: CreateWorkspaceRequest) -> Result<Workspace> {
self.client.post("/workspaces", &req).await
}
pub async fn get(&self, workspace_id: &str) -> Result<Workspace> {
self.client
.get(&format!("/workspaces/{}", workspace_id))
.await
}
pub async fn update(
&self,
workspace_id: &str,
req: UpdateWorkspaceRequest,
) -> Result<Workspace> {
self.client
.patch(&format!("/workspaces/{}", workspace_id), &req)
.await
}
pub async fn delete(&self, workspace_id: &str) -> Result<()> {
self.client
.delete(&format!("/workspaces/{}", workspace_id))
.await
}
}
pub struct WorkspaceFilesClient<'a> {
client: &'a Everruns,
}
impl<'a> WorkspaceFilesClient<'a> {
pub async fn list(
&self,
workspace_id: &str,
path: Option<&str>,
recursive: Option<bool>,
) -> Result<ListResponse<FileInfo>> {
let api_path = match path {
Some(p) => format!(
"/workspaces/{}/fs/{}",
workspace_id,
p.trim_start_matches('/')
),
None => format!("/workspaces/{}/fs", workspace_id),
};
let mut url = self.client.url(&api_path);
if let Some(true) = recursive {
url.query_pairs_mut().append_pair("recursive", "true");
}
self.client.get_url(url).await
}
pub async fn read(&self, workspace_id: &str, path: &str) -> Result<SessionFile> {
self.client
.get(&format!(
"/workspaces/{}/fs/{}",
workspace_id,
path.trim_start_matches('/')
))
.await
}
pub async fn create(
&self,
workspace_id: &str,
path: &str,
content: &str,
encoding: Option<&str>,
) -> Result<SessionFile> {
let mut req = CreateFileRequest::file(content);
if let Some(enc) = encoding {
req = req.encoding(enc);
}
self.client
.post(
&format!(
"/workspaces/{}/fs/{}",
workspace_id,
path.trim_start_matches('/')
),
&req,
)
.await
}
pub async fn create_with_options(
&self,
workspace_id: &str,
path: &str,
req: CreateFileRequest,
) -> Result<SessionFile> {
self.client
.post(
&format!(
"/workspaces/{}/fs/{}",
workspace_id,
path.trim_start_matches('/')
),
&req,
)
.await
}
pub async fn create_dir(&self, workspace_id: &str, path: &str) -> Result<SessionFile> {
self.create_with_options(workspace_id, path, CreateFileRequest::directory())
.await
}
pub async fn update(
&self,
workspace_id: &str,
path: &str,
content: &str,
encoding: Option<&str>,
) -> Result<SessionFile> {
let mut req = UpdateFileRequest::content(content);
if let Some(enc) = encoding {
req = req.encoding(enc);
}
self.client
.put(
&format!(
"/workspaces/{}/fs/{}",
workspace_id,
path.trim_start_matches('/')
),
&req,
)
.await
}
pub async fn update_with_options(
&self,
workspace_id: &str,
path: &str,
req: UpdateFileRequest,
) -> Result<SessionFile> {
self.client
.put(
&format!(
"/workspaces/{}/fs/{}",
workspace_id,
path.trim_start_matches('/')
),
&req,
)
.await
}
pub async fn delete(
&self,
workspace_id: &str,
path: &str,
recursive: Option<bool>,
) -> Result<DeleteResponse> {
let mut url = self.client.url(&format!(
"/workspaces/{}/fs/{}",
workspace_id,
path.trim_start_matches('/')
));
if let Some(true) = recursive {
url.query_pairs_mut().append_pair("recursive", "true");
}
self.client.delete_url(url).await
}
pub async fn move_file(
&self,
workspace_id: &str,
src_path: &str,
dst_path: &str,
) -> Result<SessionFile> {
let req = MoveFileRequest::new(src_path, dst_path);
self.client
.post(&format!("/workspaces/{}/fs/_/move", workspace_id), &req)
.await
}
pub async fn copy_file(
&self,
workspace_id: &str,
src_path: &str,
dst_path: &str,
) -> Result<SessionFile> {
let req = CopyFileRequest::new(src_path, dst_path);
self.client
.post(&format!("/workspaces/{}/fs/_/copy", workspace_id), &req)
.await
}
pub async fn grep(
&self,
workspace_id: &str,
pattern: &str,
path_pattern: Option<&str>,
) -> Result<ListResponse<GrepResult>> {
let mut req = GrepRequest::new(pattern);
if let Some(pp) = path_pattern {
req = req.path_pattern(pp);
}
self.client
.post(&format!("/workspaces/{}/fs/_/grep", workspace_id), &req)
.await
}
pub async fn stat(&self, workspace_id: &str, path: &str) -> Result<FileStat> {
let req = StatRequest::new(path);
self.client
.post(&format!("/workspaces/{}/fs/_/stat", workspace_id), &req)
.await
}
}
pub struct MemoriesClient<'a> {
client: &'a Everruns,
}
impl<'a> MemoriesClient<'a> {
pub async fn list(&self) -> Result<ListResponse<Memory>> {
self.client.get("/memories").await
}
pub async fn list_with_options(
&self,
search: Option<&str>,
include_archived: Option<bool>,
) -> Result<ListResponse<Memory>> {
let mut url = self.client.url("/memories");
if let Some(search) = search {
url.query_pairs_mut().append_pair("search", search);
}
if let Some(include_archived) = include_archived {
url.query_pairs_mut()
.append_pair("include_archived", &include_archived.to_string());
}
self.client.get_url(url).await
}
pub async fn create(&self, req: CreateMemoryRequest) -> Result<Memory> {
self.client.post("/memories", &req).await
}
pub async fn get(&self, memory_id: &str) -> Result<Memory> {
self.client.get(&format!("/memories/{}", memory_id)).await
}
pub async fn update(&self, memory_id: &str, req: UpdateMemoryRequest) -> Result<Memory> {
self.client
.patch(&format!("/memories/{}", memory_id), &req)
.await
}
pub async fn delete(&self, memory_id: &str) -> Result<()> {
self.client
.delete(&format!("/memories/{}", memory_id))
.await
}
pub async fn sync(&self, memory_id: &str) -> Result<Memory> {
self.client
.post::<Memory, _>(&format!("/memories/{}/sync", memory_id), &())
.await
}
pub async fn list_files(&self, memory_id: &str) -> Result<ListResponse<MemoryFileInfo>> {
self.client
.get(&format!("/memories/{}/fs", memory_id))
.await
}
pub async fn read_file(&self, memory_id: &str, path: &str) -> Result<MemoryFile> {
self.client
.get(&format!(
"/memories/{}/fs/{}",
memory_id,
path.trim_start_matches('/')
))
.await
}
pub async fn download_file(&self, memory_id: &str, path: &str) -> Result<String> {
self.client
.get_text(&format!(
"/memories/{}/fs/_/download/{}",
memory_id,
path.trim_start_matches('/')
))
.await
}
pub async fn create_file(
&self,
memory_id: &str,
path: &str,
content: &str,
encoding: Option<&str>,
) -> Result<MemoryFileInfo> {
let mut req = CreateMemoryFileRequest::file(content);
if let Some(encoding) = encoding {
req = req.encoding(encoding);
}
self.client
.post(
&format!(
"/memories/{}/fs/{}",
memory_id,
path.trim_start_matches('/')
),
&req,
)
.await
}
pub async fn create_dir(&self, memory_id: &str, path: &str) -> Result<MemoryFileInfo> {
self.client
.post(
&format!(
"/memories/{}/fs/{}",
memory_id,
path.trim_start_matches('/')
),
&CreateMemoryFileRequest::directory(),
)
.await
}
pub async fn update_file(
&self,
memory_id: &str,
path: &str,
content: &str,
encoding: Option<&str>,
) -> Result<MemoryFile> {
let mut req = UpdateMemoryFileRequest::content(content);
if let Some(encoding) = encoding {
req = req.encoding(encoding);
}
self.client
.put(
&format!(
"/memories/{}/fs/{}",
memory_id,
path.trim_start_matches('/')
),
&req,
)
.await
}
pub async fn delete_file(&self, memory_id: &str, path: &str) -> Result<()> {
self.client
.delete(&format!(
"/memories/{}/fs/{}",
memory_id,
path.trim_start_matches('/')
))
.await
}
pub async fn grep_files(
&self,
memory_id: &str,
pattern: &str,
path_pattern: Option<&str>,
) -> Result<ListResponse<MemoryGrepResult>> {
let mut req = GrepRequest::new(pattern);
if let Some(path_pattern) = path_pattern {
req = req.path_pattern(path_pattern);
}
self.client
.post(&format!("/memories/{}/fs/_/grep", memory_id), &req)
.await
}
pub async fn stat_file(&self, memory_id: &str, path: &str) -> Result<MemoryFileInfo> {
let req = StatRequest::new(path);
self.client
.post(&format!("/memories/{}/fs/_/stat", memory_id), &req)
.await
}
}
pub struct BudgetsClient<'a> {
client: &'a Everruns,
}
impl<'a> BudgetsClient<'a> {
pub async fn create(&self, req: CreateBudgetRequest) -> Result<Budget> {
self.client.post("/budgets", &req).await
}
pub async fn list(
&self,
subject_type: Option<&str>,
subject_id: Option<&str>,
) -> Result<Vec<Budget>> {
let mut url = self.client.url("/budgets");
if let Some(st) = subject_type {
url.query_pairs_mut().append_pair("subject_type", st);
}
if let Some(si) = subject_id {
url.query_pairs_mut().append_pair("subject_id", si);
}
self.client.get_url(url).await
}
pub async fn get(&self, id: &str) -> Result<Budget> {
self.client.get(&format!("/budgets/{}", id)).await
}
pub async fn update(&self, id: &str, req: UpdateBudgetRequest) -> Result<Budget> {
self.client.patch(&format!("/budgets/{}", id), &req).await
}
pub async fn delete(&self, id: &str) -> Result<()> {
self.client.delete(&format!("/budgets/{}", id)).await
}
pub async fn top_up(&self, id: &str, req: TopUpRequest) -> Result<Budget> {
self.client
.post(&format!("/budgets/{}/top-up", id), &req)
.await
}
pub async fn ledger(
&self,
id: &str,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<LedgerEntry>> {
let mut url = self.client.url(&format!("/budgets/{}/ledger", id));
if let Some(l) = limit {
url.query_pairs_mut().append_pair("limit", &l.to_string());
}
if let Some(o) = offset {
url.query_pairs_mut().append_pair("offset", &o.to_string());
}
self.client.get_url(url).await
}
pub async fn check(&self, id: &str) -> Result<BudgetCheckResult> {
self.client.get(&format!("/budgets/{}/check", id)).await
}
}
pub struct ConnectionsClient<'a> {
client: &'a Everruns,
}
impl<'a> ConnectionsClient<'a> {
pub async fn set(&self, provider: &str, api_key: &str) -> Result<Connection> {
let req = SetConnectionRequest::new(api_key);
self.client
.post(&format!("/user/connections/{}", provider), &req)
.await
}
pub async fn list(&self) -> Result<ListResponse<Connection>> {
self.client.get("/user/connections").await
}
pub async fn remove(&self, provider: &str) -> Result<()> {
self.client
.delete(&format!("/user/connections/{}", provider))
.await
}
}
impl std::fmt::Debug for Everruns {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Everruns")
.field("base_url", &self.base_url.as_str())
.field("api_key", &self.api_key)
.field(
"org_id",
&self.org_id.as_ref().and_then(|v| v.to_str().ok()),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_client() -> Everruns {
Everruns::with_base_url("test_key", "https://api.example.com").unwrap()
}
#[test]
fn test_sse_url_no_params() {
let client = test_client();
let url = client.sse_url("session_123", None, &[], &[]);
assert_eq!(
url.as_str(),
"https://api.example.com/v1/sessions/session_123/sse"
);
}
#[test]
fn test_sse_auth_headers_include_org_id() {
let client =
Everruns::with_base_url_and_org_id("test_key", "https://api.example.com", "org_123")
.unwrap();
let headers = client.auth_headers();
assert_eq!(headers["Authorization"], "test_key");
assert_eq!(headers["X-Org-Id"], "org_123");
}
#[test]
fn test_sse_url_with_since_id() {
let client = test_client();
let url = client.sse_url("session_123", Some("evt_001"), &[], &[]);
assert_eq!(
url.as_str(),
"https://api.example.com/v1/sessions/session_123/sse?since_id=evt_001"
);
}
#[test]
fn test_sse_url_exclude_expands_as_repeated_keys() {
let client = test_client();
let url = client.sse_url(
"session_123",
None,
&[],
&["output.message.delta", "reason.thinking.delta"],
);
let url_str = url.as_str();
assert!(
url_str.contains("exclude=output.message.delta"),
"URL missing first exclude: {}",
url_str
);
assert!(
url_str.contains("exclude=reason.thinking.delta"),
"URL missing second exclude: {}",
url_str
);
assert!(
!url_str.contains(','),
"URL must not use comma-separated excludes: {}",
url_str
);
assert_eq!(
url_str,
"https://api.example.com/v1/sessions/session_123/sse?exclude=output.message.delta&exclude=reason.thinking.delta"
);
}
#[test]
fn test_sse_url_single_exclude() {
let client = test_client();
let url = client.sse_url("session_123", None, &[], &["output.message.delta"]);
assert_eq!(
url.as_str(),
"https://api.example.com/v1/sessions/session_123/sse?exclude=output.message.delta"
);
}
#[test]
fn test_sse_url_combined_since_id_and_exclude() {
let client = test_client();
let url = client.sse_url(
"session_123",
Some("evt_001"),
&[],
&["output.message.delta", "reason.thinking.delta"],
);
assert_eq!(
url.as_str(),
"https://api.example.com/v1/sessions/session_123/sse?since_id=evt_001&exclude=output.message.delta&exclude=reason.thinking.delta"
);
}
#[test]
fn test_sse_url_three_exclude_values() {
let client = test_client();
let url = client.sse_url(
"session_123",
None,
&[],
&[
"output.message.delta",
"reason.thinking.delta",
"tool.started",
],
);
let url_str = url.as_str();
assert_eq!(url_str.matches("exclude=").count(), 3);
}
#[test]
fn test_sse_url_since_id_special_chars_encoded() {
let client = test_client();
let url = client.sse_url("session_123", Some("evt&id=1"), &[], &[]);
let url_str = url.as_str();
assert!(!url_str.contains("evt&id=1"));
assert!(url_str.contains("since_id=evt%26id%3D1"));
}
#[test]
fn test_sse_url_with_types() {
let client = test_client();
let url = client.sse_url(
"session_123",
None,
&["turn.started", "turn.completed"],
&[],
);
assert_eq!(
url.as_str(),
"https://api.example.com/v1/sessions/session_123/sse?types=turn.started&types=turn.completed"
);
}
#[test]
fn test_sse_url_with_types_and_exclude() {
let client = test_client();
let url = client.sse_url(
"session_123",
Some("evt_001"),
&["turn.started"],
&["output.message.delta"],
);
assert_eq!(
url.as_str(),
"https://api.example.com/v1/sessions/session_123/sse?since_id=evt_001&types=turn.started&exclude=output.message.delta"
);
}
}