agent-infra-sdk 0.1.1

Gateway-backed Rust SDK for Agent Infra APIs
Documentation
//! Unified HTTP client for Agent Infra services.
//!
//! This crate provides one Gateway-backed client for Context, Trace, Workspace,
//! Runtime Identity, Model, Deploy, and Evaluate APIs. Developers configure one
//! endpoint and one credential source; domain service topology remains private
//! to the Gateway.
#![cfg_attr(
    not(any(
        feature = "deploy",
        feature = "agents",
        feature = "evaluate",
        feature = "context",
        feature = "workspace",
        feature = "gateway",
        feature = "model",
        feature = "trace",
        feature = "runtime-identity"
    )),
    allow(dead_code, unused_variables)
)]
//!
//! # Usage
//!
//! ```rust,ignore
//! use agent_infra_sdk::InfraClient;
//!
//! let client = InfraClient::new("http://127.0.0.1:5200")?;
//!
//! let messages = client
//!     .context()
//!     .recent_messages("conv-1", Some(50), &[])
//!     .await?;
//! ```

use std::time::Duration;

#[cfg(feature = "agents")]
mod agent;
#[cfg(feature = "context")]
mod compression;
#[cfg(feature = "context")]
mod context;
#[cfg(feature = "deploy")]
mod deploy;
#[cfg(feature = "workspace")]
mod environment;
#[cfg(feature = "evaluate")]
mod evaluate;
#[cfg(feature = "gateway")]
mod gateway;
#[cfg(feature = "model")]
mod model;
mod operation;
#[cfg(feature = "runtime-identity")]
mod runtime_identity;
#[cfg(feature = "trace")]
mod trace;
mod transport;
mod transport_body;
mod transport_support;
#[cfg(feature = "workspace")]
mod workspace;

#[cfg(feature = "agents")]
pub use agent::AgentClient;
#[cfg(feature = "context")]
pub use agent_context_contract as context_contract;
#[cfg(feature = "agents")]
pub use agent_registry_contract as agent_contract;
#[cfg(feature = "runtime-identity")]
pub use agent_runtime_identity_contract as runtime_identity_contract;
#[cfg(feature = "trace")]
pub use agent_trace_contract as trace_contract;
#[cfg(feature = "workspace")]
pub use agent_workspace_contract as workspace_contract;
#[cfg(feature = "context")]
#[deprecated(note = "prompt compression belongs to runtime/context assembly")]
pub use compression::{ContextCompressionConfig, compress_context};
#[cfg(feature = "context")]
pub use context::ContextClient;
#[cfg(feature = "deploy")]
pub use deploy::DeployClient;
#[cfg(feature = "workspace")]
pub use environment::EnvironmentClient;
#[cfg(feature = "evaluate")]
pub use evaluate::EvaluateClient;
#[cfg(feature = "gateway")]
pub use gateway::{DelegationLeaseCredentials, GatewayClient, GatewayExchangeCredentials};
#[cfg(feature = "gateway")]
pub use infra_api_gateway_contract as gateway_contract;
#[cfg(feature = "model")]
pub use model::ModelClient;
pub use operation::{
    CancellationToken, OperationHandle, OperationObservation, OperationPoller, OperationProgress,
    WaitOptions,
};
#[cfg(feature = "runtime-identity")]
pub use runtime_identity::RuntimeIdentityClient;
#[cfg(feature = "trace")]
pub use trace::TraceClient;
pub use transport::{
    BearerCredential, CallOptions, ClientOptions, CredentialError, CredentialsProvider,
    InfraClientError, NoopTelemetry, RetryPolicy, StaticCredentials, TelemetryEvent,
    TelemetryObserver, TelemetryPhase,
};
#[cfg(feature = "workspace")]
pub use workspace::{VersionedContent, WorkspaceClient};

use reqwest::Client;
use transport::ServiceEndpoint;

/// Unified developer client for Agent Infra.
///
/// Every domain client shares one HTTP connection pool and sends requests to
/// the same Gateway endpoint. Domain service addresses are intentionally not
/// part of the public configuration surface.
#[derive(Clone, Debug)]
pub struct InfraClient {
    #[cfg(feature = "agents")]
    agents: AgentClient,
    #[cfg(feature = "deploy")]
    deploy: DeployClient,
    #[cfg(feature = "evaluate")]
    evaluate: EvaluateClient,
    #[cfg(feature = "gateway")]
    gateway: GatewayClient,
    #[cfg(feature = "model")]
    model: ModelClient,
    #[cfg(feature = "context")]
    context: ContextClient,
    #[cfg(feature = "trace")]
    trace: TraceClient,
    #[cfg(feature = "workspace")]
    workspace: WorkspaceClient,
    #[cfg(feature = "workspace")]
    environment: EnvironmentClient,
    #[cfg(feature = "runtime-identity")]
    runtime_identity: RuntimeIdentityClient,
}

impl InfraClient {
    /// Start configuring a client for one Agent Infra Gateway.
    pub fn builder(gateway_base_url: impl Into<String>) -> InfraClientBuilder {
        InfraClientBuilder::new(gateway_base_url)
    }

    /// Create a client with default transport policy.
    pub fn new(gateway_base_url: impl Into<String>) -> Result<Self, InfraClientError> {
        Self::builder(gateway_base_url).build()
    }

    /// Create an unauthenticated client for the local Gateway.
    pub fn local() -> Result<Self, InfraClientError> {
        Self::new("http://127.0.0.1:5200")
    }

    #[cfg(feature = "agents")]
    pub fn agents(&self) -> &AgentClient {
        &self.agents
    }

    #[cfg(feature = "deploy")]
    pub fn deploy(&self) -> &DeployClient {
        &self.deploy
    }

    #[cfg(feature = "evaluate")]
    pub fn evaluate(&self) -> &EvaluateClient {
        &self.evaluate
    }

    #[cfg(feature = "context")]
    pub fn context(&self) -> &ContextClient {
        &self.context
    }

    #[cfg(feature = "workspace")]
    pub fn workspace(&self) -> &WorkspaceClient {
        &self.workspace
    }

    #[cfg(feature = "workspace")]
    pub fn environment(&self) -> &EnvironmentClient {
        &self.environment
    }

    #[cfg(feature = "gateway")]
    pub fn gateway(&self) -> &GatewayClient {
        &self.gateway
    }

    #[cfg(feature = "model")]
    pub fn model(&self) -> &ModelClient {
        &self.model
    }

    #[cfg(feature = "trace")]
    pub fn trace(&self) -> &TraceClient {
        &self.trace
    }

    #[cfg(feature = "runtime-identity")]
    pub fn runtime_identity(&self) -> &RuntimeIdentityClient {
        &self.runtime_identity
    }
}

fn build_http_client(options: &ClientOptions) -> Result<Client, InfraClientError> {
    if options.connect_timeout.is_zero() || options.connect_timeout > Duration::from_secs(60) {
        return Err(InfraClientError::InvalidOptions {
            message: "connect_timeout must be within 1ns..=60s".into(),
        });
    }
    if options.request_timeout.is_zero() || options.request_timeout > Duration::from_secs(600) {
        return Err(InfraClientError::InvalidOptions {
            message: "request_timeout must be within 1ns..=600s".into(),
        });
    }
    if !(1..=1024).contains(&options.max_idle_connections_per_host) {
        return Err(InfraClientError::InvalidOptions {
            message: "max_idle_connections_per_host must be within 1..=1024".into(),
        });
    }
    if options.pool_idle_timeout.is_zero() || options.pool_idle_timeout > Duration::from_secs(600) {
        return Err(InfraClientError::InvalidOptions {
            message: "pool_idle_timeout must be within 1ns..=600s".into(),
        });
    }
    if !(1..=1024 * 1024 * 1024).contains(&options.max_response_bytes) {
        return Err(InfraClientError::InvalidOptions {
            message: "max_response_bytes must be within 1 byte..=1 GiB".into(),
        });
    }
    if !(1..=8).contains(&options.retry.max_attempts)
        || options.retry.base_delay > options.retry.max_delay
        || options.retry.max_delay > Duration::from_secs(30)
    {
        return Err(InfraClientError::InvalidOptions {
            message: "retry requires 1..=8 attempts and 0 <= base_delay <= max_delay <= 30s".into(),
        });
    }
    if options.user_agent.is_empty()
        || options.user_agent.len() > 256
        || reqwest::header::HeaderValue::from_str(&options.user_agent).is_err()
    {
        return Err(InfraClientError::InvalidOptions {
            message: "user_agent must be a valid 1..=256 byte HTTP header value".into(),
        });
    }
    Client::builder()
        .connect_timeout(options.connect_timeout)
        .timeout(options.request_timeout)
        .pool_idle_timeout(options.pool_idle_timeout)
        .pool_max_idle_per_host(options.max_idle_connections_per_host)
        // Service bearer credentials must not be replayed to redirects.
        .redirect(reqwest::redirect::Policy::none())
        .build()
        .map_err(InfraClientError::ClientBuild)
}

/// Builder for a Gateway-backed SDK. All typed clients share one immutable
/// `reqwest::Client`, one connection pool, and one Gateway endpoint.
#[derive(Debug, Clone)]
pub struct InfraClientBuilder {
    gateway: ServiceEndpoint,
    credentials: Option<std::sync::Arc<dyn CredentialsProvider>>,
    options: ClientOptions,
}

impl InfraClientBuilder {
    pub fn new(gateway_base_url: impl Into<String>) -> Self {
        Self {
            gateway: ServiceEndpoint::new(gateway_base_url),
            credentials: None,
            options: ClientOptions::default(),
        }
    }

    pub fn options(mut self, options: ClientOptions) -> Self {
        self.options = options;
        self
    }

    pub fn credentials(mut self, provider: std::sync::Arc<dyn CredentialsProvider>) -> Self {
        self.credentials = Some(provider);
        self
    }

    pub fn request_timeout(mut self, timeout: std::time::Duration) -> Self {
        self.options.request_timeout = timeout;
        self
    }

    pub fn max_response_bytes(mut self, limit: usize) -> Self {
        self.options.max_response_bytes = limit;
        self
    }

    pub fn retry_policy(mut self, retry: RetryPolicy) -> Self {
        self.options.retry = retry;
        self
    }

    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
        self.options.user_agent = user_agent.into();
        self
    }

    pub fn telemetry(mut self, telemetry: std::sync::Arc<dyn TelemetryObserver>) -> Self {
        self.options.telemetry = telemetry;
        self
    }

    /// Allow plaintext service endpoints only when an authenticated service
    /// mesh supplies the transport security boundary.
    pub fn trusted_mesh_http(mut self, trusted: bool) -> Self {
        self.options.trusted_mesh_http = trusted;
        self
    }

    pub fn build(self) -> Result<InfraClient, InfraClientError> {
        let http = build_http_client(&self.options)?;
        let endpoint = endpoint_with_credentials(self.gateway, &self.credentials);
        Ok(InfraClient {
            #[cfg(feature = "agents")]
            agents: AgentClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "deploy")]
            deploy: DeployClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "evaluate")]
            evaluate: EvaluateClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "gateway")]
            gateway: GatewayClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "model")]
            model: ModelClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "context")]
            context: ContextClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "trace")]
            trace: TraceClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "workspace")]
            workspace: WorkspaceClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "workspace")]
            environment: EnvironmentClient::new_with_endpoint(
                http.clone(),
                endpoint.clone(),
                self.options.clone(),
            ),
            #[cfg(feature = "runtime-identity")]
            runtime_identity: RuntimeIdentityClient::new_with_endpoint(
                http,
                endpoint,
                self.options,
            ),
        })
    }
}

fn endpoint_with_credentials(
    mut endpoint: ServiceEndpoint,
    credentials: &Option<std::sync::Arc<dyn CredentialsProvider>>,
) -> ServiceEndpoint {
    if let Some(credentials) = credentials {
        endpoint.credentials = Some(credentials.clone());
    }
    endpoint
}