Skip to main content

vtcode_core/cli/
a2a.rs

1//! A2A Protocol CLI command handlers
2//!
3//! Implements the actual logic for A2A CLI commands including:
4//! - Starting the A2A server
5//! - Discovering remote agents
6//! - Sending tasks to agents
7//! - Managing A2A agent connections
8
9use crate::a2a::cli::A2aCommands;
10
11/// Execute an A2A CLI command
12pub async fn execute_a2a_command(command: A2aCommands) -> anyhow::Result<()> {
13    match command {
14        A2aCommands::Serve {
15            host,
16            port,
17            base_url,
18            enable_push,
19        } => serve_a2a_agent(host, port, base_url, enable_push).await,
20        A2aCommands::Discover { agent_url } => discover_agent(agent_url).await,
21        A2aCommands::SendTask {
22            agent_url,
23            message,
24            stream,
25            context_id,
26        } => send_task_to_agent(agent_url, message, stream, context_id).await,
27        A2aCommands::ListTasks {
28            agent_url,
29            context_id,
30            limit,
31        } => list_agent_tasks(agent_url, context_id, limit).await,
32        A2aCommands::GetTask { agent_url, task_id } => get_agent_task(agent_url, task_id).await,
33        A2aCommands::CancelTask { agent_url, task_id } => {
34            cancel_agent_task(agent_url, task_id).await
35        }
36    }
37}
38
39/// Serve VT Code as an A2A agent
40#[cfg(feature = "a2a-server")]
41async fn serve_a2a_agent(
42    host: String,
43    port: u16,
44    base_url: Option<String>,
45    _enable_push: bool,
46) -> anyhow::Result<()> {
47    use crate::a2a::server::{A2aServerState, create_router};
48    use crate::a2a::{AgentCard, TaskManager};
49    use std::net::SocketAddr;
50
51    println!("Starting VT Code A2A Agent Server...");
52    println!("Feature: a2a-server enabled ✓");
53
54    let base_url = base_url.unwrap_or_else(|| format!("http://{}:{}", host, port));
55    let agent_card = AgentCard::vtcode_default(&base_url);
56    let task_manager = TaskManager::new();
57
58    let server_state = A2aServerState::new(task_manager, agent_card);
59    let router = create_router(server_state);
60
61    let addr = format!("{}:{}", host, port).parse::<SocketAddr>()?;
62    println!("Listening on http://{}", addr);
63    println!("Agent Card: http://{}/.well-known/agent-card.json", addr);
64    println!("JSON-RPC API: http://{}/a2a", addr);
65    println!("Streaming API: http://{}/a2a/stream", addr);
66
67    let listener = tokio::net::TcpListener::bind(addr).await?;
68    axum::serve(listener, router)
69        .with_graceful_shutdown(crate::shutdown::shutdown_signal_logged("A2A"))
70        .await?;
71
72    Ok(())
73}
74
75#[cfg(not(feature = "a2a-server"))]
76async fn serve_a2a_agent(
77    _host: String,
78    _port: u16,
79    _base_url: Option<String>,
80    _enable_push: bool,
81) -> anyhow::Result<()> {
82    anyhow::bail!(
83        "A2A server is not enabled. Build with '--features a2a-server' to enable this feature.\n\
84         Example: cargo build --release --features a2a-server"
85    )
86}
87
88/// Discover and display information about a remote A2A agent
89async fn discover_agent(agent_url: String) -> anyhow::Result<()> {
90    use crate::a2a::A2aClient;
91
92    println!("Discovering A2A agent at: {}", agent_url);
93
94    let client = A2aClient::new(&agent_url)?;
95    let agent_card = client.agent_card().await?;
96
97    println!("\n═══════════════════════════════════════════════════════════════");
98    println!("A2A Agent Discovery");
99    println!("═══════════════════════════════════════════════════════════════\n");
100
101    println!("Name: {}", agent_card.name);
102    println!("Description: {}", agent_card.description);
103    println!("Version: {}", agent_card.version);
104    println!("Protocol Version: {}", agent_card.protocol_version);
105    println!("URL: {}", agent_card.url);
106
107    if let Some(provider) = &agent_card.provider {
108        println!("\nProvider:");
109        println!("  Organization: {}", provider.organization);
110        if let Some(url) = &provider.url {
111            println!("  URL: {}", url);
112        }
113    }
114
115    if let Some(capabilities) = &agent_card.capabilities {
116        println!("\nCapabilities:");
117        println!("  Streaming: {}", capabilities.streaming);
118        println!("  Push Notifications: {}", capabilities.push_notifications);
119        println!(
120            "  State Transition History: {}",
121            capabilities.state_transition_history
122        );
123        if !capabilities.extensions.is_empty() {
124            println!("  Extensions: {:?}", capabilities.extensions);
125        }
126    }
127
128    if !agent_card.skills.is_empty() {
129        println!("\nSkills:");
130        for skill in &agent_card.skills {
131            println!("  - {}", skill.name);
132            if let Some(desc) = &skill.description {
133                println!("    Description: {}", desc);
134            }
135            if !skill.tags.is_empty() {
136                println!("    Tags: {:?}", skill.tags);
137            }
138        }
139    }
140
141    println!("\nInput Modes: {:?}", agent_card.default_input_modes);
142    println!("Output Modes: {:?}", agent_card.default_output_modes);
143
144    Ok(())
145}
146
147/// Send a task to a remote A2A agent
148async fn send_task_to_agent(
149    agent_url: String,
150    message: String,
151    stream: bool,
152    context_id: Option<String>,
153) -> anyhow::Result<()> {
154    use crate::a2a::{A2aClient, Message, rpc::MessageSendParams};
155    use futures::StreamExt;
156
157    println!("Connecting to A2A agent: {}", agent_url);
158
159    let client = A2aClient::new(&agent_url)?;
160    let msg = Message::user_text(message);
161
162    let mut params = MessageSendParams::new(msg);
163    if let Some(ctx_id) = context_id {
164        params = params.with_context_id(ctx_id);
165    }
166
167    if stream {
168        println!("Streaming task execution...\n");
169        let stream = client.stream_message(params).await?;
170        futures::pin_mut!(stream);
171
172        while let Some(event) = stream.next().await {
173            match event {
174                Ok(event) => {
175                    // Handle different event types
176                    match event {
177                        crate::a2a::rpc::StreamingEvent::Message { message, .. } => {
178                            if let Some(text) = message.parts.iter().find_map(|p| p.as_text()) {
179                                println!("Agent: {}", text);
180                            }
181                        }
182                        crate::a2a::rpc::StreamingEvent::TaskStatus { status, .. } => {
183                            println!("Status: {:?}", status.state);
184                            if let Some(msg) = status.message
185                                && let Some(text) = msg.parts.iter().find_map(|p| p.as_text())
186                            {
187                                println!("  Message: {}", text);
188                            }
189                        }
190                        crate::a2a::rpc::StreamingEvent::TaskArtifact { artifact, .. } => {
191                            println!("Artifact: {}", artifact.id);
192                        }
193                    }
194                }
195                Err(e) => {
196                    eprintln!("Stream error: {}", e);
197                    break;
198                }
199            }
200        }
201    } else {
202        println!("Sending task...\n");
203        let task = client.send_message(params).await?;
204        println!("Task created: {}", task.id);
205        println!("Status: {:?}\n", task.status.state);
206
207        if let Some(msg) = &task.status.message
208            && let Some(text) = msg.parts.iter().find_map(|p| p.as_text())
209        {
210            println!("Response: {}", text);
211        }
212
213        if !task.artifacts.is_empty() {
214            println!("\nArtifacts:");
215            for artifact in &task.artifacts {
216                println!("  - {} ({} parts)", artifact.id, artifact.parts.len());
217            }
218        }
219    }
220
221    Ok(())
222}
223
224/// List tasks from a remote A2A agent
225async fn list_agent_tasks(
226    agent_url: String,
227    context_id: Option<String>,
228    limit: u32,
229) -> anyhow::Result<()> {
230    use crate::a2a::{A2aClient, rpc::ListTasksParams};
231    use serde_json::Value;
232
233    println!("Fetching tasks from: {}", agent_url);
234
235    let client = A2aClient::new(&agent_url)?;
236    let mut params = ListTasksParams::default();
237
238    if let Some(ctx_id) = context_id {
239        params.context_id = Some(ctx_id);
240    }
241    params.page_size = Some(limit);
242
243    let result_value: Value = client.list_tasks(Some(params)).await?;
244
245    // Parse the JSON result
246    let tasks_array = result_value
247        .get("tasks")
248        .and_then(|v| v.as_array())
249        .ok_or_else(|| anyhow::anyhow!("Invalid response format"))?;
250
251    let total_size = result_value
252        .get("totalSize")
253        .and_then(|v| v.as_u64())
254        .unwrap_or(tasks_array.len() as u64);
255
256    println!(
257        "\nTasks ({} total, showing {}):",
258        total_size,
259        tasks_array.len()
260    );
261    println!("═══════════════════════════════════════════════════════════════\n");
262
263    for task_value in tasks_array {
264        if let Some(task_id) = task_value.get("id").and_then(|v| v.as_str()) {
265            println!("Task: {}", task_id);
266        }
267        if let Some(status) = task_value.get("status")
268            && let Some(state) = status.get("state").and_then(|v| v.as_str())
269        {
270            println!("  Status: {}", state);
271        }
272        if let Some(ctx_id) = task_value.get("contextId").and_then(|v| v.as_str()) {
273            println!("  Context: {}", ctx_id);
274        }
275        if let Some(artifacts) = task_value.get("artifacts").and_then(|v| v.as_array()) {
276            println!("  Artifacts: {}", artifacts.len());
277        }
278        println!();
279    }
280
281    Ok(())
282}
283
284/// Get details about a specific task
285async fn get_agent_task(agent_url: String, task_id: String) -> anyhow::Result<()> {
286    use crate::a2a::A2aClient;
287
288    println!("Fetching task {} from: {}", task_id, agent_url);
289
290    let client = A2aClient::new(&agent_url)?;
291    let task = client.get_task(task_id.clone()).await?;
292
293    println!("\n═══════════════════════════════════════════════════════════════");
294    println!("Task: {}", task.id);
295    println!("═══════════════════════════════════════════════════════════════\n");
296
297    println!("Status: {:?}", task.status.state);
298    if let Some(ctx_id) = &task.context_id {
299        println!("Context: {}", ctx_id);
300    }
301
302    if let Some(msg) = &task.status.message {
303        println!("\nLatest Message:");
304        println!("  Role: {:?}", msg.role);
305        for part in &msg.parts {
306            match part {
307                crate::a2a::types::Part::Text { text } => println!("  Text: {}", text),
308                crate::a2a::types::Part::File { file } => println!("  File: {:?}", file),
309                crate::a2a::types::Part::Data { data } => println!("  Data: {}", data),
310            }
311        }
312    }
313
314    if !task.artifacts.is_empty() {
315        println!("\nArtifacts:");
316        for artifact in &task.artifacts {
317            println!("  - {}:", artifact.id);
318            for part in &artifact.parts {
319                match part {
320                    crate::a2a::types::Part::Text { text } => {
321                        let preview =
322                            vtcode_commons::formatting::truncate_byte_budget(text, 60, "...");
323                        println!("    Text: {}", preview);
324                    }
325                    crate::a2a::types::Part::File { file } => println!("    File: {:?}", file),
326                    crate::a2a::types::Part::Data { data } => {
327                        let s = data.to_string();
328                        let preview =
329                            vtcode_commons::formatting::truncate_byte_budget(&s, 60, "...");
330                        println!("    Data: {}", preview);
331                    }
332                }
333            }
334        }
335    }
336
337    if !task.history.is_empty() {
338        println!("\nHistory ({} messages):", task.history.len());
339        for (i, msg) in task.history.iter().enumerate() {
340            println!("  {}. {:?}: {} parts", i + 1, msg.role, msg.parts.len());
341        }
342    }
343
344    Ok(())
345}
346
347/// Cancel a running task
348async fn cancel_agent_task(agent_url: String, task_id: String) -> anyhow::Result<()> {
349    use crate::a2a::A2aClient;
350
351    println!("Canceling task {} at: {}", task_id, agent_url);
352
353    let client = A2aClient::new(&agent_url)?;
354    client.cancel_task(task_id).await?;
355
356    println!("Task cancellation requested successfully.");
357
358    Ok(())
359}
360
361#[cfg(test)]
362mod tests {
363    // Intentionally empty or specialized imports if needed later
364
365    #[tokio::test]
366    async fn test_discover_agent_display() {
367        // This is a simple display test - actual client functionality is tested in integration tests
368    }
369}