vv-agent 0.1.3

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::sync::Arc;

use serde_json::{json, Value};

mod common;

use common::{print_run, runtime_log_handler, ExampleConfig};
use vv_agent::constants::TASK_FINISH_TOOL_NAME;
use vv_agent::{
    build_default_registry, AgentDefinition, AgentSDKClient, AgentSDKOptions, ToolContext,
    ToolExecutionResult, ToolRegistry,
};

const TICKET_STORE_TOOL_NAME: &str = "_ticket_store";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ExampleConfig::load();
    config.ensure_workspace()?;
    let sample_log = config.workspace.join("logs/app.log");
    if !sample_log.exists() {
        std::fs::create_dir_all(sample_log.parent().expect("log parent"))?;
        std::fs::write(
            &sample_log,
            [
                "2026-02-18T01:21:03Z ERROR payment retry timeout order=ORD-1001",
                "2026-02-18T01:21:12Z WARN redis connection unstable",
                "2026-02-18T01:22:09Z ERROR email provider 503 campaign=SPRING",
                "2026-02-18T01:22:44Z ERROR search index lag exceeds threshold",
            ]
            .join("\n"),
        )?;
    }

    let mut agent = AgentDefinition::default_for_model(config.model.clone());
    agent.description = concat!(
        "你是 SRE 值班助理. 先从日志中提炼问题, 再调用自定义工单工具落盘,",
        "最后给出处理优先级建议。"
    )
    .to_string();
    agent.backend = Some(config.backend.clone());
    agent.max_cycles = 18;
    agent.enable_todo_management = true;
    agent.extra_tool_names = vec![TICKET_STORE_TOOL_NAME.to_string()];

    let client = AgentSDKClient::new_with_agent(
        AgentSDKOptions {
            settings_file: config.settings_file,
            default_backend: config.backend,
            workspace: config.workspace,
            tool_registry_factory: Some(Arc::new(build_registry_with_ticket_tool)),
            log_handler: runtime_log_handler(config.verbose),
            ..AgentSDKOptions::default()
        },
        agent,
    );
    let run = client.run(format!(
        concat!(
            "请读取 logs/app.log, 提炼至少 3 个问题并调用 `{}` action=create 写入工单。",
            "然后调用 `{}` action=list 检查当前工单。",
            "最后调用 `{}` 输出简洁结论。"
        ),
        TICKET_STORE_TOOL_NAME, TICKET_STORE_TOOL_NAME, TASK_FINISH_TOOL_NAME
    ))?;
    print_run(&run)
}

fn build_registry_with_ticket_tool() -> ToolRegistry {
    let mut registry = build_default_registry();
    registry
        .register_tool_with_parameters(
            TICKET_STORE_TOOL_NAME,
            "Manage local support tickets. Actions: create, list.",
            json!({
                "type": "object",
                "properties": {
                    "action": {"type": "string", "description": "One of: create, list."},
                    "title": {"type": "string", "description": "Ticket title."},
                    "description": {"type": "string", "description": "Ticket description."},
                    "severity": {"type": "string", "description": "low/medium/high."}
                },
                "required": ["action"]
            }),
            Arc::new(ticket_store),
        )
        .expect("register ticket tool");
    registry
}

fn ticket_store(
    context: &mut ToolContext,
    arguments: &vv_agent::types::ToolArguments,
) -> ToolExecutionResult {
    let action = arguments
        .get("action")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .trim()
        .to_ascii_lowercase();
    let db_path = match context.resolve_workspace_path("artifacts/tickets.json") {
        Ok(path) => path,
        Err(error) => return ToolExecutionResult::error("", error),
    };
    let mut tickets = std::fs::read_to_string(&db_path)
        .ok()
        .and_then(|content| serde_json::from_str::<Vec<Value>>(&content).ok())
        .unwrap_or_default();
    match action.as_str() {
        "create" => {
            let title = arguments
                .get("title")
                .and_then(Value::as_str)
                .unwrap_or_default()
                .trim();
            if title.is_empty() {
                return ToolExecutionResult::error(
                    "",
                    json!({"ok": false, "error": "`title` is required"}).to_string(),
                );
            }
            let ticket = json!({
                "id": format!("T-{:08x}", tickets.len() + 1),
                "title": title,
                "description": arguments.get("description").and_then(Value::as_str).unwrap_or_default(),
                "severity": arguments.get("severity").and_then(Value::as_str).unwrap_or("medium"),
                "status": "open",
            });
            tickets.push(ticket.clone());
            if let Some(parent) = db_path.parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            let _ = std::fs::write(&db_path, serde_json::to_string_pretty(&tickets).unwrap());
            ToolExecutionResult::success(
                "",
                json!({"ok": true, "action": action, "ticket": ticket, "total": tickets.len()})
                    .to_string(),
            )
        }
        "list" => ToolExecutionResult::success(
            "",
            json!({"ok": true, "action": action, "count": tickets.len(), "tickets": tickets})
                .to_string(),
        ),
        _ => ToolExecutionResult::error(
            "",
            json!({"ok": false, "error": "Invalid action. Use create/list."}).to_string(),
        ),
    }
}