vv-agent 0.6.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::collections::BTreeMap;
use std::sync::{
    atomic::{AtomicU64, Ordering},
    Arc,
};

use chrono::{SecondsFormat, Utc};
use serde_json::{json, Value};

use crate::tools::base::{ToolContext, ToolSpec};
use crate::tools::common::stringify_tool_arg;
use crate::types::{ToolArguments, ToolDirective, ToolExecutionResult, ToolResultStatus};

pub(crate) fn todo_write_tool() -> ToolSpec {
    let mut spec = ToolSpec::new(
        "todo_write",
        "Replace the current todo list for the task.",
        Arc::new(todo_write),
    );
    if let Some(schema) = super::super::schemas::schema_for("todo_write") {
        spec.schema = schema;
    }
    spec
}

pub fn todo_write(context: &mut ToolContext, arguments: &ToolArguments) -> ToolExecutionResult {
    let Some(todos) = arguments.get("todos").and_then(Value::as_array) else {
        return todo_write_error("`todos` must be an array", "invalid_todos_payload");
    };

    let mut existing_map = BTreeMap::new();
    if let Some(existing_todos) = context
        .shared_state
        .get("todo_list")
        .and_then(Value::as_array)
    {
        for item in existing_todos {
            let Some(item_id) = item.get("id").map(todo_value_to_string) else {
                continue;
            };
            if !item_id.is_empty() {
                existing_map.insert(item_id, item.clone());
            }
        }
    }

    let now = Utc::now().to_rfc3339_opts(SecondsFormat::Micros, false);
    let mut new_todo_list = Vec::new();

    for (index, raw_todo_item) in todos.iter().enumerate() {
        let Some(todo_object) = raw_todo_item.as_object() else {
            return todo_write_error(
                format!("TODO item at index {index} must be an object"),
                "invalid_todo_item",
            );
        };

        let title = todo_object
            .get("title")
            .map(todo_value_to_string)
            .unwrap_or_default();
        if title.is_empty() {
            return todo_write_error(
                format!("TODO item at index {index} is missing `title`"),
                "todo_title_required",
            );
        }

        let status = todo_object
            .get("status")
            .map(todo_value_to_string)
            .unwrap_or_else(|| "pending".to_string())
            .to_ascii_lowercase();
        if !matches!(status.as_str(), "pending" | "in_progress" | "completed") {
            return todo_write_error(
                format!("TODO item {title} has invalid status {status}"),
                "invalid_todo_status",
            );
        }

        let priority = todo_object
            .get("priority")
            .map(todo_value_to_string)
            .unwrap_or_else(|| "medium".to_string())
            .to_ascii_lowercase();
        if !matches!(priority.as_str(), "low" | "medium" | "high") {
            return todo_write_error(
                format!("TODO item {title} has invalid priority {priority}"),
                "invalid_todo_priority",
            );
        }

        let item_id = todo_object
            .get("id")
            .map(todo_value_to_string)
            .filter(|value| !value.is_empty())
            .unwrap_or_else(generate_todo_id);
        let created_at = existing_map
            .get(&item_id)
            .and_then(|item| item.get("created_at"))
            .map(todo_value_to_string)
            .filter(|value| !value.is_empty())
            .unwrap_or_else(|| now.clone());

        new_todo_list.push(json!({
            "id": item_id,
            "title": title,
            "status": status,
            "priority": priority,
            "created_at": created_at,
            "updated_at": now.clone(),
        }));
    }

    let in_progress_count = new_todo_list
        .iter()
        .filter(|todo| {
            todo.get("status")
                .and_then(Value::as_str)
                .is_some_and(|status| status == "in_progress")
        })
        .count();
    if in_progress_count > 1 {
        return todo_write_error(
            "Only one TODO item can be in_progress at a time",
            "multiple_in_progress_todos",
        );
    }

    context
        .shared_state
        .insert("todo_list".to_string(), Value::Array(new_todo_list.clone()));
    ToolExecutionResult::success(
        "",
        json!({
            "action": "write",
            "todos": new_todo_list,
            "count": new_todo_list.len(),
            "message": format!("TODO list updated successfully with {} items", new_todo_list.len()),
        })
        .to_string(),
    )
}

pub fn todo_read(context: &mut ToolContext, _arguments: &ToolArguments) -> ToolExecutionResult {
    let todos = context
        .shared_state
        .entry("todo_list".to_string())
        .or_insert_with(|| Value::Array(Vec::new()))
        .as_array()
        .cloned()
        .unwrap_or_default();
    ToolExecutionResult::success(
        "",
        json!({
            "action": "read",
            "todos": todos,
            "count": todos.len(),
        })
        .to_string(),
    )
}

fn todo_write_error(message: impl Into<String>, error_code: &str) -> ToolExecutionResult {
    ToolExecutionResult {
        tool_call_id: String::new(),
        content: json!({
            "error": message.into(),
            "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,
    }
}

fn todo_value_to_string(value: &Value) -> String {
    stringify_tool_arg(Some(value), "").trim().to_string()
}

fn generate_todo_id() -> String {
    static TODO_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
    let counter = TODO_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
    let timestamp = Utc::now().timestamp_nanos_opt().unwrap_or_default() as u64;
    format!("{:08x}", (timestamp ^ counter) & 0xffff_ffff)
}