vv-agent 0.8.0

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::collections::BTreeMap;

use jsonschema::{Draft, Validator};
use serde::Serialize;
use serde_json::{json, Value};

use crate::types::{Metadata, ToolCall, ToolDirective, ToolExecutionResult, ToolResultStatus};

pub(crate) const INVALID_TOOL_ARGUMENTS_ERROR_CODE: &str = "invalid_tool_arguments";
pub(crate) const INVALID_TOOL_ARGUMENTS_MESSAGE: &str =
    "Tool arguments do not match the declared schema";

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
struct ArgumentValidationIssue {
    instance_path: String,
    schema_path: String,
    rule: String,
}

pub(crate) fn close_object_schemas(schema: &Value) -> Value {
    let mut closed = schema.clone();
    close_object_schemas_in_place(&mut closed);
    closed
}

pub(crate) fn validator_for_tool_schema(schema: &Value) -> Result<Validator, String> {
    let parameters = schema
        .get("function")
        .and_then(|function| function.get("parameters"))
        .ok_or_else(|| "tool schema must contain function.parameters".to_string())?;
    validator_for_parameters(parameters)
}

pub(crate) fn validator_for_parameters(schema: &Value) -> Result<Validator, String> {
    jsonschema::options()
        .with_draft(Draft::Draft202012)
        .build(schema)
        .map_err(|error| format!("invalid tool parameters schema: {error}"))
}

pub(crate) fn invalid_tool_arguments_result(
    validator: &Validator,
    call: &ToolCall,
) -> Option<ToolExecutionResult> {
    let instance = Value::Object(call.arguments.clone().into_iter().collect());
    let mut issues = validator
        .iter_errors(&instance)
        .map(|error| ArgumentValidationIssue {
            instance_path: error.instance_path().to_string(),
            schema_path: error.schema_path().to_string(),
            rule: error.kind().keyword().to_string(),
        })
        .collect::<Vec<_>>();
    issues.sort();
    issues.dedup();
    if issues.is_empty() {
        return None;
    }

    let mut metadata = Metadata::new();
    metadata.insert(
        "error_code".to_string(),
        json!(INVALID_TOOL_ARGUMENTS_ERROR_CODE),
    );
    metadata.insert("issue_count".to_string(), json!(issues.len()));
    Some(ToolExecutionResult {
        tool_call_id: call.id.clone(),
        content: json!({
            "ok": false,
            "error": INVALID_TOOL_ARGUMENTS_MESSAGE,
            "error_code": INVALID_TOOL_ARGUMENTS_ERROR_CODE,
            "issues": issues,
        })
        .to_string(),
        status: ToolResultStatus::Error,
        directive: ToolDirective::Continue,
        error_code: Some(INVALID_TOOL_ARGUMENTS_ERROR_CODE.to_string()),
        metadata,
        image_url: None,
        image_path: None,
    })
}

fn close_object_schemas_in_place(value: &mut Value) {
    match value {
        Value::Object(object) => {
            if object.get("type").and_then(Value::as_str) == Some("object") {
                object
                    .entry("additionalProperties".to_string())
                    .or_insert(Value::Bool(false));
            }
            for child in object.values_mut() {
                close_object_schemas_in_place(child);
            }
        }
        Value::Array(items) => {
            for child in items {
                close_object_schemas_in_place(child);
            }
        }
        _ => {}
    }
}

pub(crate) fn schema_map_with_closed_objects(
    schemas: BTreeMap<String, Value>,
) -> BTreeMap<String, Value> {
    schemas
        .into_iter()
        .map(|(name, schema)| (name, close_object_schemas(&schema)))
        .collect()
}