agent-infra-sdk 0.2.0

Unified Rust SDK for Gateway-backed and local Agent Infra APIs
//! Typed client for the unified Agent Infra administration namespace.

use agent_context_contract::{MessagesResponse, SearchMessagesRequest};
use agent_registry_contract::{
    Agent, AgentId, AgentPage, CreateAgentRequest, ListAgentsRequest, UpdateAgentRequest,
};
use agent_runtime_identity_contract::{
    CreateRuntimeIdentityRequest, CreateRuntimeIdentityResponse, ListRuntimeIdentitiesRequest,
    ListRuntimeIdentitiesResponse, RuntimeIdentity, RuntimeIdentityId, TransitionIdentityRequest,
};
use reqwest::{Client, Url};

use crate::transport::{
    CallOptions, ClientOptions, HttpTransport, InfraClientError, ServiceEndpoint,
};

const ADMIN_SERVICE: &str = "agent-infra-admin";
const ADMIN_AUDIENCE: &str = "agent-infra";
const ADMIN_AGENTS_PATH: &str = "/admin/api/agents";
const ADMIN_CONTEXT_SEARCH_PATH: &str = "/admin/api/context/messages/search";
const ADMIN_RUNTIMES_PATH: &str = "/admin/api/runtime-identities";
const ADMIN_RUNTIME_LIST_PATH: &str = "/admin/api/runtime-identities:list";

/// Unified administrator facade for Agent, Context, and Runtime Identity resources.
#[derive(Clone, Debug)]
pub struct AdminClient {
    transport: HttpTransport,
}

impl AdminClient {
    pub(crate) fn new_with_endpoint(
        http: Client,
        endpoint: ServiceEndpoint,
        options: ClientOptions,
    ) -> Self {
        let endpoint = endpoint.with_default_credential_audience(ADMIN_AUDIENCE);
        Self {
            transport: HttpTransport::new_with_options(http, ADMIN_SERVICE, endpoint, options),
        }
    }

    pub async fn list_agents(
        &self,
        request: &ListAgentsRequest,
    ) -> Result<AgentPage, InfraClientError> {
        let mut url = Url::parse("http://agent-infra-admin.invalid").expect("static URL is valid");
        {
            let mut query = url.query_pairs_mut();
            if let Some(status) = request.status {
                query.append_pair(
                    "status",
                    match status {
                        agent_registry_contract::AgentStatus::Active => "active",
                        agent_registry_contract::AgentStatus::Archived => "archived",
                    },
                );
            }
            if let Some(agent_id) = &request.related_agent_id {
                query.append_pair("relatedAgentId", agent_id.as_str());
            }
            if let Some(runtime_id) = &request.related_runtime_id {
                query.append_pair("relatedRuntimeId", runtime_id);
            }
            if let Some(user_id) = &request.related_user_id {
                query.append_pair("relatedUserId", user_id);
            }
            if let Some(cursor) = &request.cursor {
                query.append_pair("cursor", cursor);
            }
            if let Some(limit) = request.limit {
                query.append_pair("limit", &limit.to_string());
            }
        }
        let path = match url.query() {
            Some(query) if !query.is_empty() => format!("{ADMIN_AGENTS_PATH}?{query}"),
            _ => ADMIN_AGENTS_PATH.to_string(),
        };
        self.transport.get_json(&path).await
    }

    pub async fn create_agent(
        &self,
        request: &CreateAgentRequest,
    ) -> Result<Agent, InfraClientError> {
        self.transport.post_json(ADMIN_AGENTS_PATH, request).await
    }

    pub async fn update_agent(
        &self,
        agent_id: &AgentId,
        request: &UpdateAgentRequest,
    ) -> Result<Agent, InfraClientError> {
        self.transport
            .patch_json_with_options(
                &format!("{ADMIN_AGENTS_PATH}/{}", agent_id.as_str()),
                request,
                CallOptions::default().idempotent(true),
            )
            .await
    }

    pub async fn search_context(
        &self,
        request: &SearchMessagesRequest,
    ) -> Result<MessagesResponse, InfraClientError> {
        self.transport
            .post_json_idempotent(ADMIN_CONTEXT_SEARCH_PATH, request)
            .await
    }

    pub async fn list_runtime_identities(
        &self,
        request: &ListRuntimeIdentitiesRequest,
    ) -> Result<ListRuntimeIdentitiesResponse, InfraClientError> {
        self.transport
            .post_json_idempotent(ADMIN_RUNTIME_LIST_PATH, request)
            .await
    }

    pub async fn create_runtime_identity(
        &self,
        request: &CreateRuntimeIdentityRequest,
    ) -> Result<CreateRuntimeIdentityResponse, InfraClientError> {
        self.transport.post_json(ADMIN_RUNTIMES_PATH, request).await
    }

    pub async fn transition_runtime_identity(
        &self,
        identity_id: &RuntimeIdentityId,
        action: &str,
        request: &TransitionIdentityRequest,
    ) -> Result<RuntimeIdentity, InfraClientError> {
        if !matches!(action, "activate" | "suspend" | "resume" | "revoke") {
            return Err(InfraClientError::Protocol {
                service: ADMIN_SERVICE,
                message: "unknown runtime identity administration action".into(),
            });
        }
        self.transport
            .post_json(
                &format!("{ADMIN_RUNTIMES_PATH}/{}:{action}", identity_id.as_str()),
                request,
            )
            .await
    }
}