vv-agent 0.2.0

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
mod state;

use std::sync::Arc;

use serde_json::Value;

use crate::skills::normalize_skill_list;
use crate::tools::base::{ToolContext, ToolSpec};
use crate::tools::common::{tool_error_with_code, tool_result};
use crate::tools::schemas;
use crate::types::{ToolArguments, ToolDirective, ToolExecutionResult, ToolResultStatus};

use state::{append_activation_log, append_unique_string};

pub fn activate_skill(context: &mut ToolContext, arguments: &ToolArguments) -> ToolExecutionResult {
    let spec = activate_skill_tool();
    (spec.handler)(context, arguments)
}

pub(crate) fn activate_skill_tool() -> ToolSpec {
    let mut spec = ToolSpec::new(
        "activate_skill",
        "Activate a skill from the current task's available skill list.",
        Arc::new(|context, arguments| {
            let skill_name = value_to_trimmed_string(arguments.get("skill_name"));
            let reason = value_to_trimmed_string(arguments.get("reason"));
            if skill_name.is_empty() {
                return tool_error_with_code("`skill_name` is required", "skill_name_required");
            }
            let raw_skills = context
                .shared_state
                .get("available_skills")
                .or_else(|| context.metadata.get("available_skills"));
            let entries = normalize_skill_list(raw_skills, Some(&context.workspace), true);
            if entries.is_empty() {
                return tool_error_with_code(
                    "No skills are configured for this task",
                    "no_skills_configured",
                );
            }
            let Some(entry) = entries.into_iter().find(|entry| entry.name == skill_name) else {
                return tool_error_with_code(
                    format!("Skill '{skill_name}' is not allowed for this task"),
                    "skill_not_allowed",
                );
            };
            if let Some(error) = entry.load_error {
                return tool_error_with_code(
                    format!("Skill '{skill_name}' is invalid: {error}"),
                    "skill_invalid",
                );
            }

            let instructions = entry
                .instructions
                .filter(|text| !text.is_empty())
                .unwrap_or_else(|| {
                    format!("Skill '{skill_name}' is activated, but no instruction text is available. Please inspect the skill files or provide explicit instructions.")
                });
            append_unique_string(
                &mut context.shared_state,
                "active_skills",
                entry.name.clone(),
            );
            append_activation_log(
                &mut context.shared_state,
                entry.name.clone(),
                reason.clone(),
                context.cycle_index,
            );
            let mut payload = serde_json::Map::from_iter([
                ("status".to_string(), Value::String("activated".to_string())),
                ("skill_name".to_string(), Value::String(entry.name.clone())),
                (
                    "message".to_string(),
                    Value::String(format!(
                        "Skill '{}' has been activated. Follow the instructions below.",
                        entry.name
                    )),
                ),
                ("instructions".to_string(), Value::String(instructions)),
            ]);
            if !entry.description.is_empty() {
                payload.insert("description".to_string(), Value::String(entry.description));
            }
            if let Some(location) = entry.location {
                payload.insert("location".to_string(), Value::String(location));
            }
            if let Some(allowed_tools) = entry.allowed_tools {
                payload.insert("allowed_tools".to_string(), Value::String(allowed_tools));
            }
            if !reason.is_empty() {
                payload.insert("reason".to_string(), Value::String(reason));
            }
            tool_result(
                ToolResultStatus::Success,
                Value::Object(payload),
                None,
                ToolDirective::Continue,
            )
        }),
    );
    if let Some(schema) = schemas::schema_for("activate_skill") {
        spec.schema = schema;
    }
    spec
}

fn value_to_trimmed_string(value: Option<&Value>) -> String {
    match value {
        Some(Value::String(text)) => text.trim().to_string(),
        Some(Value::Null) | None => String::new(),
        Some(Value::Bool(true)) => "True".to_string(),
        Some(Value::Bool(false)) => "False".to_string(),
        Some(Value::Number(number)) => number.to_string().trim().to_string(),
        Some(other) => other.to_string().trim().to_string(),
    }
}