Skip to main content

vv_agent/
cli.rs

1mod args;
2mod logging;
3mod output;
4mod task;
5
6use std::env;
7use std::fs;
8use std::io::{BufRead, Write};
9use std::path::Path;
10use std::sync::Arc;
11
12use crate::app_server::processor::MessageProcessor;
13use crate::app_server::protocol::{
14    generate_app_server_json_schema_bundle, generate_app_server_typescript_bundle, AppClientInfo,
15    JsonRpcMessage, ThreadStartParams, TurnStartParams, UserInput,
16};
17use crate::app_server::test_support::{finish_response, scripted_app_server_client};
18use crate::app_server::transport::stdio::{parse_jsonl_message, serialize_jsonl_message};
19use crate::app_server::transport::ConnectionId;
20use crate::config::build_vv_llm_from_local_settings;
21use crate::runtime::AgentRuntime;
22use crate::workspace::LocalWorkspaceBackend;
23
24pub use self::args::{
25    parse_cli_args_from, parse_cli_args_from_with_default_settings, parse_cli_command_from,
26    parse_cli_command_from_with_default_settings, AppServerCliCommand, CliArgs, CliCommand,
27    DebugCliCommand,
28};
29use self::logging::build_cli_log_handler;
30pub use self::output::result_payload;
31use self::task::generate_task_id;
32pub use self::task::{build_cli_task, build_cli_task_from_resolved};
33
34pub fn main() -> Result<(), String> {
35    let raw_args = env::args().collect::<Vec<_>>();
36    if raw_args
37        .iter()
38        .skip(1)
39        .any(|arg| matches!(arg.as_str(), "--help" | "-h"))
40    {
41        println!("{}", args::help_text());
42        return Ok(());
43    }
44    let command = parse_cli_command_from(raw_args)?;
45    match command {
46        CliCommand::Run(args) => run_task(args),
47        CliCommand::AppServer(command) => run_app_server_command(command),
48        CliCommand::Debug(command) => run_debug_command(command),
49    }
50}
51
52fn run_task(args: CliArgs) -> Result<(), String> {
53    let (llm, resolved) =
54        build_vv_llm_from_local_settings(&args.settings_file, &args.backend, &args.model, 90.0)
55            .map_err(|err| err.to_string())?;
56
57    let mut runtime = AgentRuntime::new(llm)
58        .with_settings_file(args.settings_file.clone())
59        .with_default_backend(args.backend.clone());
60    runtime.default_workspace = Some(args.workspace.clone());
61    runtime.workspace_backend = Arc::new(LocalWorkspaceBackend::new(args.workspace.clone()));
62    runtime.log_handler = build_cli_log_handler(args.verbose);
63
64    let task = build_cli_task_from_resolved(&args, &resolved, generate_task_id())?;
65    let result = runtime.run(task).map_err(|err| err.to_string())?;
66    let payload = result_payload(&result, &resolved);
67    let output = serde_json::to_string_pretty(&payload).map_err(|err| err.to_string())?;
68    println!("{output}");
69    Ok(())
70}
71
72fn run_app_server_command(command: AppServerCliCommand) -> Result<(), String> {
73    match command {
74        AppServerCliCommand::ListenStdio => run_app_server_stdio(),
75        AppServerCliCommand::GenerateTs { out } => write_schema_bundle(
76            &out,
77            generate_app_server_typescript_bundle().map_err(|error| error.to_string())?,
78            None,
79        ),
80        AppServerCliCommand::GenerateJsonSchema { out } => write_schema_bundle(
81            &out,
82            generate_app_server_json_schema_bundle().map_err(|error| error.to_string())?,
83            Some("json"),
84        ),
85    }
86}
87
88fn run_debug_command(command: DebugCliCommand) -> Result<(), String> {
89    match command {
90        DebugCliCommand::AppServerSendMessage { message } => run_debug_app_server_message(message),
91    }
92}
93
94fn run_app_server_stdio() -> Result<(), String> {
95    let runtime = tokio::runtime::Builder::new_current_thread()
96        .enable_time()
97        .build()
98        .map_err(|error| error.to_string())?;
99    runtime.block_on(async {
100        let (mut processor, mut outgoing) = MessageProcessor::new_for_tests(128);
101        let connection_id = ConnectionId::new(1);
102        let stdin = std::io::stdin();
103        let mut stdout = std::io::stdout();
104        for line in stdin.lock().lines() {
105            let line = line.map_err(|error| error.to_string())?;
106            let Some(message) =
107                parse_jsonl_message(&line).map_err(|error| error.message().to_string())?
108            else {
109                continue;
110            };
111            processor.process_message(connection_id, message).await;
112            while let Ok(envelope) = outgoing.try_recv() {
113                let line = serialize_jsonl_message(&envelope.message)
114                    .map_err(|error| error.message().to_string())?;
115                stdout
116                    .write_all(line.as_bytes())
117                    .map_err(|error| error.to_string())?;
118                stdout.flush().map_err(|error| error.to_string())?;
119            }
120        }
121        Ok(())
122    })
123}
124
125fn run_debug_app_server_message(message: String) -> Result<(), String> {
126    let runtime = tokio::runtime::Builder::new_current_thread()
127        .enable_time()
128        .build()
129        .map_err(|error| error.to_string())?;
130    runtime.block_on(async {
131        let mut client = scripted_app_server_client(vec![finish_response(&message)]);
132        client
133            .initialize(AppClientInfo {
134                name: "debug-cli".to_string(),
135                title: Some("Debug CLI".to_string()),
136                version: Some(env!("CARGO_PKG_VERSION").to_string()),
137            })
138            .await
139            .map_err(|error| error.to_string())?;
140        let thread = client
141            .start_thread(ThreadStartParams {
142                cwd: None,
143                title: Some("debug".to_string()),
144                model: Some("demo-model".to_string()),
145                ephemeral: true,
146            })
147            .await
148            .map_err(|error| error.to_string())?
149            .thread;
150        client
151            .start_turn(TurnStartParams {
152                thread_id: thread.id,
153                input: vec![UserInput {
154                    text: message.clone(),
155                }],
156                model: Some("demo-model".to_string()),
157            })
158            .await
159            .map_err(|error| error.to_string())?;
160        println!(
161            "{}",
162            serde_json::to_string(&serde_json::json!({
163                "method": "debug/input",
164                "params": { "message": message }
165            }))
166            .map_err(|error| error.to_string())?
167        );
168        while let Some(next) = client.next_message().await {
169            println!(
170                "{}",
171                serde_json::to_string(&next).map_err(|error| error.to_string())?
172            );
173            if matches!(
174                next,
175                JsonRpcMessage::Notification(notification)
176                    if notification.method == "turn/completed"
177            ) {
178                break;
179            }
180        }
181        Ok(())
182    })
183}
184
185fn write_schema_bundle(
186    out: &Path,
187    bundle: crate::app_server::protocol::SchemaBundle,
188    extension: Option<&str>,
189) -> Result<(), String> {
190    fs::create_dir_all(out).map_err(|error| error.to_string())?;
191    for (name, content) in bundle {
192        let file_name = match extension {
193            Some(extension) => format!("{name}.{extension}"),
194            None => name,
195        };
196        fs::write(out.join(file_name), content).map_err(|error| error.to_string())?;
197    }
198    Ok(())
199}