awaken-server 0.6.0

Multi-protocol HTTP server with SSE, mailbox, and protocol adapters for Awaken
Documentation
use std::collections::HashSet;

use awaken_runtime::AgentResolver;
use awaken_runtime::registry::resolve::RegistrySetResolver;
use awaken_runtime::registry::{
    RegistryDiagnostic, RegistrySet, RegistryValidationError, diagnose_agent_spec,
};
use awaken_server_contract::{
    AgentSpec, PreparedSkillSpecs, SkillSpec, is_skill_allowed_tool_pattern,
    parse_skill_allowed_tools, validate_skill_allowed_tool_pattern,
};

use crate::services::agent_catalog::check_catalog_errors;

use super::{ConfigRuntimeError, ConfigRuntimeManager};

impl ConfigRuntimeManager {
    pub(super) fn prepare_skill_specs(
        &self,
        skills: &[SkillSpec],
    ) -> Result<Option<Box<dyn PreparedSkillSpecs>>, ConfigRuntimeError> {
        match (&self.skill_spec_sink, skills.is_empty()) {
            (Some(sink), _) => sink
                .prepare_skill_specs(skills.to_vec())
                .map(Some)
                .map_err(ConfigRuntimeError::InvalidConfig),
            (None, true) => Ok(None),
            (None, false) => Err(ConfigRuntimeError::InvalidConfig(
                "skills config records require a configured skill_spec_sink".into(),
            )),
        }
    }

    pub(super) fn validate_skill_allowed_tools(
        &self,
        known_tool_ids: &HashSet<String>,
        skills: &[SkillSpec],
    ) -> Result<(), ConfigRuntimeError> {
        for skill in skills {
            for token in &skill.allowed_tools {
                let parsed = parse_skill_allowed_tools(token).map_err(|error| {
                    ConfigRuntimeError::InvalidConfig(format!(
                        "skill '{}' has invalid allowed_tools entry '{}': {error}",
                        skill.id, token
                    ))
                })?;
                if parsed.len() != 1 || parsed[0].raw != *token {
                    return Err(ConfigRuntimeError::InvalidConfig(format!(
                        "skill '{}' allowed_tools entry '{}' must contain exactly one token",
                        skill.id, token
                    )));
                }
                let parsed = &parsed[0];
                if is_skill_allowed_tool_pattern(&parsed.tool_id) {
                    validate_skill_allowed_tool_pattern(&parsed.tool_id).map_err(|error| {
                        ConfigRuntimeError::InvalidConfig(format!(
                            "skill '{}' has invalid allowed_tools matcher '{}': {error}",
                            skill.id, token
                        ))
                    })?;
                    continue;
                }
                if !known_tool_ids.contains(&parsed.tool_id) {
                    return Err(ConfigRuntimeError::InvalidConfig(format!(
                        "skill '{}' allowed_tools references unknown tool '{}'",
                        skill.id, parsed.tool_id
                    )));
                }
            }
        }
        Ok(())
    }

    pub(super) fn validate_candidate(
        &self,
        candidate: &RegistrySet,
        local_agents: &[AgentSpec],
        skills: &[SkillSpec],
    ) -> Result<(), ConfigRuntimeError> {
        check_catalog_errors(local_agents).map_err(ConfigRuntimeError::InvalidConfig)?;
        let mut diagnostics = Vec::new();
        for model_id in candidate.models.model_ids() {
            let Some(model) = candidate.models.get_model(&model_id) else {
                continue;
            };
            let provider_id = model.provider_id;
            if candidate.providers.get_provider(&provider_id).is_none() {
                diagnostics.push(RegistryDiagnostic::ModelMissingProvider {
                    model_id,
                    provider_id,
                });
            }
        }
        for pool_id in candidate.models.pool_ids() {
            let Some(pool) = candidate.models.get_pool(&pool_id) else {
                continue;
            };
            for member in pool.members {
                if candidate.models.get_model(&member.model_id).is_none() {
                    diagnostics.push(RegistryDiagnostic::ModelPoolMissingModel {
                        pool_id: pool_id.clone(),
                        model_id: member.model_id,
                    });
                }
            }
        }
        for agent in local_agents {
            diagnostics.extend(diagnose_agent_spec(candidate, agent));
        }
        if !diagnostics.is_empty() {
            let err = RegistryValidationError::from_diagnostics(diagnostics).to_string();
            return Err(ConfigRuntimeError::InvalidConfig(err));
        }

        let resolver = RegistrySetResolver::new(candidate.clone());
        let mut known_tool_ids = candidate
            .tools
            .tool_ids()
            .into_iter()
            .collect::<HashSet<_>>();
        for agent in local_agents {
            if agent.uses_remote_backend() {
                continue;
            }
            let resolved = resolver.resolve(&agent.id).map_err(|error| {
                ConfigRuntimeError::InvalidConfig(format!("{}: {error}", agent.id))
            })?;
            known_tool_ids.extend(resolved.tools.keys().cloned());
        }
        self.validate_skill_allowed_tools(&known_tool_ids, skills)?;
        Ok(())
    }
}