vv-agent 0.4.8

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
mod args;
mod logging;
mod output;
mod task;

use std::env;
use std::fs;
use std::io::{BufRead, Write};
use std::path::Path;
use std::sync::Arc;

use crate::app_server::processor::MessageProcessor;
use crate::app_server::protocol::{
    generate_app_server_json_schema_bundle, generate_app_server_typescript_bundle, AppClientInfo,
    JsonRpcMessage, ThreadStartParams, TurnStartParams, UserInput,
};
use crate::app_server::test_support::{finish_response, scripted_app_server_client};
use crate::app_server::transport::stdio::{parse_jsonl_message, serialize_jsonl_message};
use crate::app_server::transport::ConnectionId;
use crate::config::build_vv_llm_from_local_settings;
use crate::runtime::AgentRuntime;
use crate::workspace::LocalWorkspaceBackend;

pub use self::args::{
    parse_cli_args_from, parse_cli_args_from_with_default_settings, parse_cli_command_from,
    parse_cli_command_from_with_default_settings, AppServerCliCommand, CliArgs, CliCommand,
    DebugCliCommand,
};
use self::logging::build_cli_log_handler;
pub use self::output::result_payload;
use self::task::generate_task_id;
pub use self::task::{build_cli_task, build_cli_task_from_resolved};

pub fn main() -> Result<(), String> {
    let raw_args = env::args().collect::<Vec<_>>();
    if raw_args
        .iter()
        .skip(1)
        .any(|arg| matches!(arg.as_str(), "--help" | "-h"))
    {
        println!("{}", args::help_text());
        return Ok(());
    }
    let command = parse_cli_command_from(raw_args)?;
    match command {
        CliCommand::Run(args) => run_task(args),
        CliCommand::AppServer(command) => run_app_server_command(command),
        CliCommand::Debug(command) => run_debug_command(command),
    }
}

fn run_task(args: CliArgs) -> Result<(), String> {
    let (llm, resolved) =
        build_vv_llm_from_local_settings(&args.settings_file, &args.backend, &args.model, 90.0)
            .map_err(|err| err.to_string())?;

    let mut runtime = AgentRuntime::new(llm)
        .with_settings_file(args.settings_file.clone())
        .with_default_backend(args.backend.clone());
    runtime.default_workspace = Some(args.workspace.clone());
    runtime.workspace_backend = Arc::new(LocalWorkspaceBackend::new(args.workspace.clone()));
    runtime.log_handler = build_cli_log_handler(args.verbose);

    let task = build_cli_task_from_resolved(&args, &resolved, generate_task_id())?;
    let result = runtime.run(task).map_err(|err| err.to_string())?;
    let payload = result_payload(&result, &resolved);
    let output = serde_json::to_string_pretty(&payload).map_err(|err| err.to_string())?;
    println!("{output}");
    Ok(())
}

fn run_app_server_command(command: AppServerCliCommand) -> Result<(), String> {
    match command {
        AppServerCliCommand::ListenStdio => run_app_server_stdio(),
        AppServerCliCommand::GenerateTs { out } => write_schema_bundle(
            &out,
            generate_app_server_typescript_bundle().map_err(|error| error.to_string())?,
            None,
        ),
        AppServerCliCommand::GenerateJsonSchema { out } => write_schema_bundle(
            &out,
            generate_app_server_json_schema_bundle().map_err(|error| error.to_string())?,
            Some("json"),
        ),
    }
}

fn run_debug_command(command: DebugCliCommand) -> Result<(), String> {
    match command {
        DebugCliCommand::AppServerSendMessage { message } => run_debug_app_server_message(message),
    }
}

fn run_app_server_stdio() -> Result<(), String> {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_time()
        .build()
        .map_err(|error| error.to_string())?;
    runtime.block_on(async {
        let (mut processor, mut outgoing) = MessageProcessor::new_for_tests(128);
        let connection_id = ConnectionId::new(1);
        let stdin = std::io::stdin();
        let mut stdout = std::io::stdout();
        for line in stdin.lock().lines() {
            let line = line.map_err(|error| error.to_string())?;
            let Some(message) =
                parse_jsonl_message(&line).map_err(|error| error.message().to_string())?
            else {
                continue;
            };
            processor.process_message(connection_id, message).await;
            while let Ok(envelope) = outgoing.try_recv() {
                let line = serialize_jsonl_message(&envelope.message)
                    .map_err(|error| error.message().to_string())?;
                stdout
                    .write_all(line.as_bytes())
                    .map_err(|error| error.to_string())?;
                stdout.flush().map_err(|error| error.to_string())?;
            }
        }
        Ok(())
    })
}

fn run_debug_app_server_message(message: String) -> Result<(), String> {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_time()
        .build()
        .map_err(|error| error.to_string())?;
    runtime.block_on(async {
        let mut client = scripted_app_server_client(vec![finish_response(&message)]);
        client
            .initialize(AppClientInfo {
                name: "debug-cli".to_string(),
                title: Some("Debug CLI".to_string()),
                version: Some(env!("CARGO_PKG_VERSION").to_string()),
            })
            .await
            .map_err(|error| error.to_string())?;
        let thread = client
            .start_thread(ThreadStartParams {
                cwd: None,
                title: Some("debug".to_string()),
                model: Some("demo-model".to_string()),
                ephemeral: true,
            })
            .await
            .map_err(|error| error.to_string())?
            .thread;
        client
            .start_turn(TurnStartParams {
                thread_id: thread.id,
                input: vec![UserInput {
                    text: message.clone(),
                }],
                model: Some("demo-model".to_string()),
            })
            .await
            .map_err(|error| error.to_string())?;
        println!(
            "{}",
            serde_json::to_string(&serde_json::json!({
                "method": "debug/input",
                "params": { "message": message }
            }))
            .map_err(|error| error.to_string())?
        );
        while let Some(next) = client.next_message().await {
            println!(
                "{}",
                serde_json::to_string(&next).map_err(|error| error.to_string())?
            );
            if matches!(
                next,
                JsonRpcMessage::Notification(notification)
                    if notification.method == "turn/completed"
            ) {
                break;
            }
        }
        Ok(())
    })
}

fn write_schema_bundle(
    out: &Path,
    bundle: crate::app_server::protocol::SchemaBundle,
    extension: Option<&str>,
) -> Result<(), String> {
    fs::create_dir_all(out).map_err(|error| error.to_string())?;
    for (name, content) in bundle {
        let file_name = match extension {
            Some(extension) => format!("{name}.{extension}"),
            None => name,
        };
        fs::write(out.join(file_name), content).map_err(|error| error.to_string())?;
    }
    Ok(())
}