vv-agent 0.7.0

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

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

pub(crate) fn needs_tool_call_id(value: &str) -> bool {
    let stripped = value.trim();
    stripped.is_empty() || stripped == "pending"
}

pub(crate) fn apply_tool_use_behavior(
    task: &crate::types::AgentTask,
    call: &ToolCall,
    result: &mut ToolExecutionResult,
) -> Option<CompletionReason> {
    if result.directive != ToolDirective::Continue || result.status != ToolResultStatus::Success {
        return None;
    }
    let behavior = task
        .metadata
        .get("_vv_agent_tool_use_behavior")
        .and_then(serde_json::Value::as_str)
        .unwrap_or("run_llm_again");
    let should_stop = match behavior {
        "stop_on_first_tool" => true,
        "stop_at_tool_names" => task
            .metadata
            .get("_vv_agent_stop_at_tool_names")
            .and_then(serde_json::Value::as_array)
            .is_some_and(|names| {
                names
                    .iter()
                    .any(|name| name.as_str() == Some(call.name.as_str()))
            }),
        _ => false,
    };
    if should_stop {
        result.directive = ToolDirective::Finish;
        return Some(if behavior == "stop_on_first_tool" {
            CompletionReason::StopOnFirstTool
        } else {
            CompletionReason::StopAtToolName
        });
    }
    None
}

pub(crate) fn skipped_tool_result(
    call: &ToolCall,
    error_code: &str,
    message: &str,
) -> ToolExecutionResult {
    ToolExecutionResult {
        tool_call_id: call.id.clone(),
        content: serde_json::json!({
            "ok": false,
            "error": message,
            "error_code": error_code,
        })
        .to_string(),
        status: ToolResultStatus::Error,
        directive: ToolDirective::Continue,
        error_code: Some(error_code.to_string()),
        metadata: BTreeMap::new(),
        image_url: None,
        image_path: None,
    }
}

pub(super) fn image_notification_from_tool_result(
    result: &ToolExecutionResult,
    include_image: bool,
) -> Option<Message> {
    if !include_image {
        return None;
    }
    if let Some(image_url) = &result.image_url {
        let content = result
            .image_path
            .as_deref()
            .map(|image_path| format!("[Image loaded] {image_path}"))
            .unwrap_or_default();
        let mut image_message = Message::user(content);
        image_message.image_url = Some(image_url.clone());
        image_message.metadata = result.metadata.clone();
        return Some(image_message);
    }
    result
        .image_path
        .as_deref()
        .map(|image_path| Message::user(format!("[Image loaded] {image_path}")))
}