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}