vv-agent 0.6.2

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

use serde_json::{json, Value};
use vv_agent::prompt::{build_system_prompt_with_options, BuildSystemPromptOptions};
use vv_agent::{
    build_default_registry, build_vv_llm_from_local_settings, AgentRuntime, AgentStatus, AgentTask,
    SubAgentConfig,
};

#[test]
#[ignore = "live API call; run with VV_AGENT_RUN_LIVE_TESTS=1 cargo test --test live_sub_task_wait -- --ignored"]
fn live_agent_waits_for_background_sub_task_completion() {
    if !live_enabled() {
        eprintln!("Live tests are disabled. Set VV_AGENT_RUN_LIVE_TESTS=1 to run.");
        return;
    }

    let settings_file = live_settings_path();
    assert!(
        settings_file.exists(),
        "live settings file is missing: {}",
        settings_file.display()
    );
    let backend = env::var("VV_AGENT_LIVE_BACKEND").unwrap_or_else(|_| "moonshot".to_string());
    let model = env::var("VV_AGENT_LIVE_MODEL").unwrap_or_else(|_| "kimi-k2.6".to_string());
    let workspace = env::temp_dir().join("vv-agent-rs-live-sub-task-wait");
    std::fs::create_dir_all(&workspace).expect("workspace");
    let (llm, resolved) = build_vv_llm_from_local_settings(&settings_file, &backend, &model, 90.0)
        .expect("build live vv-llm client");
    let mut runtime = AgentRuntime::new(llm)
        .with_tool_registry(build_default_registry())
        .with_settings_file(settings_file)
        .with_default_backend(backend.clone())
        .with_sub_agent_timeout_seconds(90.0);
    runtime.default_workspace = Some(workspace.clone());

    let mut available_sub_agents = BTreeMap::new();
    available_sub_agents.insert(
        "slow-researcher".to_string(),
        "Sleeps briefly, then returns the requested token.".to_string(),
    );
    let parent_prompt = build_system_prompt_with_options(
        "You are testing sub-agent orchestration. Follow these exact steps and do not answer directly. \
Step 1: call create_sub_task with agent_id='slow-researcher', task_description asking the child \
to sleep briefly then return the token WAIT-SUBTASK-OK, and wait_for_completion=false. \
Step 2: after you receive the task_id, call sub_task_status with that task_id, wait_for_completion=true, \
check_interval_seconds=300, max_wait_seconds=120, and detail_level='snapshot'. \
Step 3: after the status result is completed, call task_finish with a message containing WAIT-SUBTASK-OK.",
        BuildSystemPromptOptions {
            language: "en-US".to_string(),
            available_sub_agents,
            workspace: Some(workspace.clone()),
            ..BuildSystemPromptOptions::default()
        },
    );
    let child_prompt = "You are the slow-researcher sub-agent. First call bash with command 'sleep 2' and timeout 10. \
After bash succeeds, call task_finish with exactly: WAIT-SUBTASK-OK.";

    let mut sub_agent =
        SubAgentConfig::new(model.clone(), "Sleeps briefly and returns WAIT-SUBTASK-OK.");
    sub_agent.backend = Some(backend);
    sub_agent.system_prompt = Some(child_prompt.to_string());
    sub_agent.max_cycles = 4;

    let mut task = AgentTask::new(
        "live_sub_task_wait",
        resolved.model_id,
        parent_prompt,
        "Run the exact live sub-task wait verification now.",
    );
    task.max_cycles = 8;
    task.sub_agents
        .insert("slow-researcher".to_string(), sub_agent);
    task.metadata
        .insert("language".to_string(), Value::String("en-US".to_string()));

    let result = runtime.run(task).expect("run live task");

    assert_eq!(result.status, AgentStatus::Completed, "{:?}", result.error);
    let tool_calls = result
        .cycles
        .iter()
        .flat_map(|cycle| cycle.tool_calls.iter())
        .collect::<Vec<_>>();
    let create_calls = tool_calls
        .iter()
        .filter(|call| call.name == "create_sub_task")
        .collect::<Vec<_>>();
    let status_calls = tool_calls
        .iter()
        .filter(|call| call.name == "sub_task_status")
        .collect::<Vec<_>>();
    assert!(
        !create_calls.is_empty(),
        "live parent did not call create_sub_task"
    );
    assert!(
        !status_calls.is_empty(),
        "live parent did not call sub_task_status"
    );
    assert_eq!(
        create_calls[0].arguments.get("wait_for_completion"),
        Some(&json!(false))
    );
    assert!(status_calls
        .iter()
        .any(|call| call.arguments.get("wait_for_completion") == Some(&json!(true))));

    let status_call_ids = status_calls
        .iter()
        .map(|call| call.id.as_str())
        .collect::<Vec<_>>();
    let status_payloads = result
        .cycles
        .iter()
        .flat_map(|cycle| cycle.tool_results.iter())
        .filter(|result| status_call_ids.contains(&result.tool_call_id.as_str()))
        .map(|result| serde_json::from_str::<Value>(&result.content).expect("status payload"))
        .collect::<Vec<_>>();
    assert!(
        !status_payloads.is_empty(),
        "live run did not return sub_task_status payload"
    );
    let completed_status = status_payloads.iter().find(|payload| {
        payload["wait_for_completion"] == json!(true)
            && payload["wait_exceeded"] == json!(false)
            && payload["tasks"][0]["status"] == json!("completed")
    });
    assert!(
        completed_status.is_some(),
        "status payloads did not include completed wait result: {status_payloads:?}"
    );
    let completed_status = completed_status.expect("completed status payload");
    assert_eq!(completed_status["running_task_ids"], json!([]));
    assert_eq!(
        completed_status["suggested_next_check_after_seconds"],
        json!(300)
    );
    assert!(serde_json::to_string(completed_status)
        .expect("status json")
        .contains("WAIT-SUBTASK-OK"));
    assert!(result
        .final_answer
        .as_deref()
        .unwrap_or_default()
        .contains("WAIT-SUBTASK-OK"));
}

fn live_enabled() -> bool {
    env::var("VV_AGENT_RUN_LIVE_TESTS")
        .ok()
        .map(|value| {
            matches!(
                value.trim().to_ascii_lowercase().as_str(),
                "1" | "true" | "yes" | "on"
            )
        })
        .unwrap_or(false)
}

fn live_settings_path() -> PathBuf {
    env::var("VV_AGENT_LIVE_SETTINGS_JSON")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/dev_settings.json")
        })
}