unified-agent-api 0.3.5

Agent-agnostic facade and registry for wrapper backends
Documentation
use std::{collections::BTreeSet, future::Future, pin::Pin, sync::Arc};

use super::{
    harness::new_harness_adapter, mcp_management, ClaudeCodeBackend, AGENT_KIND,
    CAP_ARTIFACTS_FINAL_TEXT_V1, CAP_SESSION_HANDLE_V1, CAP_TOOLS_RESULTS_V1,
    CAP_TOOLS_STRUCTURED_V1, EXT_ADD_DIRS_V1, EXT_EXTERNAL_SANDBOX_V1, EXT_NON_INTERACTIVE,
};
use crate::{
    backend_harness::BackendDefaults,
    mcp::{
        normalize_mcp_add_request, normalize_mcp_get_request, normalize_mcp_remove_request,
        AgentWrapperMcpAddRequest, AgentWrapperMcpCommandOutput, AgentWrapperMcpGetRequest,
        AgentWrapperMcpListRequest, AgentWrapperMcpRemoveRequest, CAPABILITY_MCP_ADD_V1,
        CAPABILITY_MCP_GET_V1, CAPABILITY_MCP_LIST_V1, CAPABILITY_MCP_REMOVE_V1,
    },
    AgentWrapperBackend, AgentWrapperCapabilities, AgentWrapperError, AgentWrapperKind,
    AgentWrapperRunControl, AgentWrapperRunHandle, AgentWrapperRunRequest,
    EXT_AGENT_API_CONFIG_MODEL_V1,
};

use super::super::session_selectors::{EXT_SESSION_FORK_V1, EXT_SESSION_RESUME_V1};

fn unsupported_capability<T>(
    agent_kind: String,
    capability: &'static str,
) -> Pin<Box<dyn Future<Output = Result<T, AgentWrapperError>> + Send + 'static>> {
    Box::pin(async move {
        Err(AgentWrapperError::UnsupportedCapability {
            agent_kind,
            capability: capability.to_string(),
        })
    })
}

impl AgentWrapperBackend for ClaudeCodeBackend {
    fn kind(&self) -> AgentWrapperKind {
        AgentWrapperKind(AGENT_KIND.to_string())
    }

    fn capabilities(&self) -> AgentWrapperCapabilities {
        let mut ids = BTreeSet::new();
        ids.insert("agent_api.run".to_string());
        ids.insert("agent_api.events".to_string());
        ids.insert("agent_api.events.live".to_string());
        ids.insert(crate::CAPABILITY_CONTROL_CANCEL_V1.to_string());
        ids.insert(CAP_TOOLS_STRUCTURED_V1.to_string());
        ids.insert(CAP_TOOLS_RESULTS_V1.to_string());
        ids.insert(CAP_ARTIFACTS_FINAL_TEXT_V1.to_string());
        ids.insert(CAP_SESSION_HANDLE_V1.to_string());
        ids.insert(EXT_AGENT_API_CONFIG_MODEL_V1.to_string());
        ids.insert("backend.claude_code.print_stream_json".to_string());
        ids.insert(EXT_ADD_DIRS_V1.to_string());
        ids.insert(EXT_NON_INTERACTIVE.to_string());
        ids.insert(EXT_SESSION_RESUME_V1.to_string());
        ids.insert(EXT_SESSION_FORK_V1.to_string());
        if super::claude_mcp_list_supported_on_target() {
            ids.insert(CAPABILITY_MCP_LIST_V1.to_string());
        }
        if super::claude_mcp_get_supported_on_target() {
            ids.insert(CAPABILITY_MCP_GET_V1.to_string());
            if self.config.allow_mcp_write {
                ids.insert(CAPABILITY_MCP_ADD_V1.to_string());
                ids.insert(CAPABILITY_MCP_REMOVE_V1.to_string());
            }
        }
        if self.config.allow_external_sandbox_exec {
            ids.insert(EXT_EXTERNAL_SANDBOX_V1.to_string());
        }
        AgentWrapperCapabilities { ids }
    }

    fn mcp_list(
        &self,
        request: AgentWrapperMcpListRequest,
    ) -> Pin<
        Box<
            dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
                + Send
                + '_,
        >,
    > {
        if !self.capabilities().contains(CAPABILITY_MCP_LIST_V1) {
            return unsupported_capability(
                self.kind().as_str().to_string(),
                CAPABILITY_MCP_LIST_V1,
            );
        }

        let config = self.config.clone();
        Box::pin(async move {
            mcp_management::run_claude_mcp(
                config,
                mcp_management::claude_mcp_list_argv(),
                request.context,
            )
            .await
        })
    }

    fn mcp_get(
        &self,
        request: AgentWrapperMcpGetRequest,
    ) -> Pin<
        Box<
            dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
                + Send
                + '_,
        >,
    > {
        if !self.capabilities().contains(CAPABILITY_MCP_GET_V1) {
            return unsupported_capability(self.kind().as_str().to_string(), CAPABILITY_MCP_GET_V1);
        }

        let request = match normalize_mcp_get_request(request) {
            Ok(request) => request,
            Err(err) => return Box::pin(async move { Err(err) }),
        };
        let config = self.config.clone();
        let argv = mcp_management::claude_mcp_get_argv(&request.name);
        Box::pin(async move { mcp_management::run_claude_mcp(config, argv, request.context).await })
    }

    fn mcp_add(
        &self,
        request: AgentWrapperMcpAddRequest,
    ) -> Pin<
        Box<
            dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
                + Send
                + '_,
        >,
    > {
        if !self.capabilities().contains(CAPABILITY_MCP_ADD_V1) {
            return unsupported_capability(self.kind().as_str().to_string(), CAPABILITY_MCP_ADD_V1);
        }

        let request = match normalize_mcp_add_request(request) {
            Ok(request) => request,
            Err(err) => return Box::pin(async move { Err(err) }),
        };
        let config = self.config.clone();
        let argv = match mcp_management::claude_mcp_add_argv(&request.name, &request.transport) {
            Ok(argv) => argv,
            Err(err) => return Box::pin(async move { Err(err) }),
        };
        Box::pin(async move { mcp_management::run_claude_mcp(config, argv, request.context).await })
    }

    fn mcp_remove(
        &self,
        request: AgentWrapperMcpRemoveRequest,
    ) -> Pin<
        Box<
            dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
                + Send
                + '_,
        >,
    > {
        if !self.capabilities().contains(CAPABILITY_MCP_REMOVE_V1) {
            return unsupported_capability(
                self.kind().as_str().to_string(),
                CAPABILITY_MCP_REMOVE_V1,
            );
        }

        let request = match normalize_mcp_remove_request(request) {
            Ok(request) => request,
            Err(err) => return Box::pin(async move { Err(err) }),
        };
        let config = self.config.clone();
        let argv = mcp_management::claude_mcp_remove_argv(&request.name);
        Box::pin(async move { mcp_management::run_claude_mcp(config, argv, request.context).await })
    }

    fn run(
        &self,
        request: AgentWrapperRunRequest,
    ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunHandle, AgentWrapperError>> + Send + '_>>
    {
        let config = self.config.clone();
        let allow_flag_preflight = Arc::clone(&self.allow_flag_preflight);
        let run_start_cwd = std::env::current_dir().ok();
        Box::pin(async move {
            let adapter = Arc::new(new_harness_adapter(
                config.clone(),
                run_start_cwd,
                None,
                allow_flag_preflight,
            ));
            let defaults = BackendDefaults {
                env: config.env,
                default_timeout: config.default_timeout,
            };

            crate::backend_harness::run_harnessed_backend(adapter, defaults, request).await
        })
    }

    fn run_control(
        &self,
        request: AgentWrapperRunRequest,
    ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunControl, AgentWrapperError>> + Send + '_>>
    {
        if !self
            .capabilities()
            .contains(crate::CAPABILITY_CONTROL_CANCEL_V1)
        {
            return unsupported_capability(
                self.kind().as_str().to_string(),
                crate::CAPABILITY_CONTROL_CANCEL_V1,
            );
        }

        let config = self.config.clone();
        let allow_flag_preflight = Arc::clone(&self.allow_flag_preflight);
        let run_start_cwd = std::env::current_dir().ok();
        Box::pin(async move {
            let termination_state = Arc::new(super::super::termination::TerminationState::new());
            let request_termination: Option<Arc<dyn Fn() + Send + Sync + 'static>> = Some({
                let termination_state = Arc::clone(&termination_state);
                Arc::new(move || termination_state.request())
            });

            let adapter = Arc::new(new_harness_adapter(
                config.clone(),
                run_start_cwd,
                Some(termination_state),
                allow_flag_preflight,
            ));
            let defaults = BackendDefaults {
                env: config.env,
                default_timeout: config.default_timeout,
            };

            crate::backend_harness::run_harnessed_backend_control(
                adapter,
                defaults,
                request,
                request_termination,
            )
            .await
        })
    }
}