acp-runtime 0.1.2

Rust SDK for the Agent Communication Protocol (ACP)
Documentation
// Copyright 2026 ACP Project
// Licensed under the Apache License, Version 2.0
// See LICENSE file for details.

use std::sync::Arc;

use serde_json::{Map, Value};

use crate::agent::AcpAgent;
use crate::errors::{AcpError, AcpResult};
use crate::messages::DeliveryMode;
use crate::overlay::{
    BusinessHandler, OverlayInboundAdapter, OverlayOutboundAdapter, PassthroughHandler,
    invalid_overlay_request,
};

pub const WELL_KNOWN_CACHE_CONTROL: &str = "public, max-age=300";

#[derive(Debug, Clone)]
pub struct OverlayHttpResponse {
    pub status_code: u16,
    pub body: Map<String, Value>,
}

#[derive(Clone)]
pub struct OverlayFrameworkRuntime {
    pub agent: AcpAgent,
    pub base_url: String,
    pub inbound_adapter: OverlayInboundAdapter,
    pub outbound_adapter: OverlayOutboundAdapter,
}

#[derive(Clone)]
pub struct OverlayConfig {
    pub agent: AcpAgent,
    pub base_url: String,
    pub passthrough_handler: Option<PassthroughHandler>,
}

#[derive(Clone)]
pub struct OverlayClient {
    pub agent: AcpAgent,
    pub outbound_adapter: OverlayOutboundAdapter,
}

impl OverlayFrameworkRuntime {
    pub fn create(
        agent: AcpAgent,
        base_url: &str,
        business_handler: BusinessHandler,
        passthrough_handler: Option<PassthroughHandler>,
    ) -> AcpResult<Self> {
        let normalized_base_url = base_url.trim();
        if normalized_base_url.is_empty() {
            return Err(AcpError::Validation("base_url is required".to_string()));
        }
        let inbound_adapter =
            OverlayInboundAdapter::new(agent.clone(), business_handler, passthrough_handler);
        let outbound_adapter = OverlayOutboundAdapter::new(agent.clone());
        Ok(Self {
            agent,
            base_url: normalized_base_url.trim_end_matches('/').to_string(),
            inbound_adapter,
            outbound_adapter,
        })
    }

    pub fn handle_message_body(&mut self, body: &Value) -> OverlayHttpResponse {
        let Some(body) = body.as_object() else {
            return OverlayHttpResponse {
                status_code: 400,
                body: invalid_overlay_request("Expected JSON object request body"),
            };
        };
        match self.inbound_adapter.handle_request(body) {
            Ok(response) => OverlayHttpResponse {
                status_code: 200,
                body: response,
            },
            Err(exc) => OverlayHttpResponse {
                status_code: 400,
                body: invalid_overlay_request(exc.to_string()),
            },
        }
    }

    pub fn well_known_document(&self) -> AcpResult<Map<String, Value>> {
        self.agent
            .build_well_known_document(Some(&self.base_url), None)
    }

    pub fn well_known_headers() -> Map<String, Value> {
        let mut headers = Map::new();
        headers.insert(
            "Cache-Control".to_string(),
            Value::String(WELL_KNOWN_CACHE_CONTROL.to_string()),
        );
        headers
    }

    pub fn identity_document_payload(&self) -> Map<String, Value> {
        let mut payload = Map::new();
        payload.insert(
            "identity_document".to_string(),
            Value::Object(self.agent.identity_document.clone()),
        );
        payload
    }

    #[allow(clippy::too_many_arguments)]
    pub fn send_business_payload(
        &mut self,
        payload: Map<String, Value>,
        target_base_url: Option<&str>,
        recipient_agent_id: Option<&str>,
        context: Option<String>,
        delivery_mode: Option<DeliveryMode>,
        expires_in_seconds: i64,
    ) -> AcpResult<Map<String, Value>> {
        let result = self.outbound_adapter.send_business_payload(
            payload,
            target_base_url,
            recipient_agent_id,
            context,
            delivery_mode,
            expires_in_seconds,
        )?;
        Ok(send_result_to_map(result))
    }

    pub fn send_acp(
        &mut self,
        target_url: &str,
        payload: Map<String, Value>,
        recipient_agent_id: Option<&str>,
        context: Option<String>,
        delivery_mode: Option<DeliveryMode>,
        expires_in_seconds: i64,
    ) -> AcpResult<Map<String, Value>> {
        self.send_business_payload(
            payload,
            Some(target_url),
            recipient_agent_id,
            context,
            delivery_mode,
            expires_in_seconds,
        )
    }

    pub fn handle(
        request_body: &Value,
        business_handler: BusinessHandler,
        config: OverlayConfig,
    ) -> OverlayHttpResponse {
        let runtime = Self::create(
            config.agent,
            &config.base_url,
            business_handler,
            config.passthrough_handler,
        );
        match runtime {
            Ok(mut runtime) => runtime.handle_message_body(request_body),
            Err(exc) => OverlayHttpResponse {
                status_code: 400,
                body: invalid_overlay_request(exc.to_string()),
            },
        }
    }
}

impl OverlayClient {
    pub fn create(agent: AcpAgent) -> Self {
        Self {
            outbound_adapter: OverlayOutboundAdapter::new(agent.clone()),
            agent,
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub fn send_acp(
        &mut self,
        target_url: &str,
        payload: Map<String, Value>,
        recipient_agent_id: Option<&str>,
        context: Option<String>,
        delivery_mode: Option<DeliveryMode>,
        expires_in_seconds: i64,
    ) -> AcpResult<Map<String, Value>> {
        let result = self.outbound_adapter.send_business_payload(
            payload,
            Some(target_url),
            recipient_agent_id,
            context,
            delivery_mode,
            expires_in_seconds,
        )?;
        Ok(send_result_to_map(result))
    }
}

pub fn acp_overlay_inbound(
    mut agent: AcpAgent,
    handler: Arc<dyn Fn(&Map<String, Value>) -> Option<Map<String, Value>> + Send + Sync>,
    passthrough: bool,
) -> impl FnMut(&Map<String, Value>) -> AcpResult<Map<String, Value>> {
    let passthrough_handler = if passthrough {
        Some(handler.clone())
    } else {
        None
    };
    move |payload: &Map<String, Value>| {
        let mut inbound =
            OverlayInboundAdapter::new(agent.clone(), handler.clone(), passthrough_handler.clone());
        let response = inbound.handle_request(payload);
        agent = inbound.agent;
        response
    }
}

fn send_result_to_map(result: crate::overlay::OverlaySendResult) -> Map<String, Value> {
    let mut map = Map::new();
    if let Some(target) = result.target {
        map.insert(
            "target".to_string(),
            serde_json::json!({
                "agent_id": target.agent_id,
                "base_url": target.base_url,
                "well_known_url": target.well_known_url,
                "identity_document_url": target.identity_document_url,
            }),
        );
    } else {
        map.insert("target".to_string(), Value::Null);
    }
    map.insert(
        "send_result".to_string(),
        serde_json::to_value(&result.send_result).unwrap_or(Value::Null),
    );
    map
}