1use std::collections::HashMap;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::sync::{Arc, Mutex};
28
29use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
30use tokio::sync::mpsc;
31
32use super::{
33 BlockControllerRuntimeStatus, BlockInputUnion, Controller, STATUS_DONE, STATUS_INIT,
34 STATUS_RUNNING,
35};
36use super::core;
37use super::health::HealthMonitor;
38use crate::backend::eventbus::EventBus;
39use crate::backend::storage::filestore::FileStore;
40use crate::backend::storage::store::Store;
41use crate::backend::wps;
42
43pub const ACP_OUTPUT_SUBJECT: &str = "output";
45
46pub const BLOCK_CONTROLLER_ACP: &str = "acp";
47
48struct AcpInner {
50 proc_status: String,
51 proc_exit_code: i32,
52 status_version: i32,
53 session_id: Option<String>,
54 current_pid: Option<u32>,
55 stdin_tx: Option<mpsc::Sender<String>>,
56 kill_tx: Option<tokio::sync::oneshot::Sender<bool>>,
57 pending_prompt: Option<String>,
59}
60
61pub struct AcpController {
63 #[allow(dead_code)]
64 tab_id: String,
65 block_id: String,
66 inner: Arc<Mutex<AcpInner>>,
67 broker: Option<Arc<wps::Broker>>,
68 event_bus: Option<Arc<EventBus>>,
69 wstore: Option<Arc<Store>>,
70 filestore: Option<Arc<FileStore>>,
71 health_monitor: Arc<HealthMonitor>,
72 next_rpc_id: Arc<AtomicU64>,
74}
75
76impl AcpController {
77 pub fn new(
78 tab_id: String,
79 block_id: String,
80 broker: Option<Arc<wps::Broker>>,
81 event_bus: Option<Arc<EventBus>>,
82 wstore: Option<Arc<Store>>,
83 filestore: Option<Arc<FileStore>>,
84 ) -> Self {
85 let health_monitor = Arc::new(HealthMonitor::new(
86 block_id.clone(),
87 broker.clone(),
88 ));
89 Self {
90 tab_id,
91 block_id,
92 inner: Arc::new(Mutex::new(AcpInner {
93 proc_status: STATUS_INIT.to_string(),
94 proc_exit_code: 0,
95 status_version: 0,
96 session_id: None,
97 current_pid: None,
98 stdin_tx: None,
99 kill_tx: None,
100 pending_prompt: None,
101 })),
102 broker,
103 event_bus,
104 wstore,
105 filestore,
106 health_monitor,
107 next_rpc_id: Arc::new(AtomicU64::new(1)),
108 }
109 }
110
111 fn next_id(&self) -> u64 {
112 self.next_rpc_id.fetch_add(1, Ordering::Relaxed)
113 }
114
115 fn set_status(inner: &mut AcpInner, status: &str) {
116 inner.proc_status = status.to_string();
117 inner.status_version += 1;
118 }
119
120 fn get_status_snapshot(&self) -> BlockControllerRuntimeStatus {
121 let inner = self.inner.lock().unwrap();
122 BlockControllerRuntimeStatus {
123 blockid: self.block_id.clone(),
124 version: inner.status_version,
125 shellprocstatus: inner.proc_status.clone(),
126 shellprocconnname: "local".to_string(),
127 shellprocexitcode: inner.proc_exit_code,
128 spawn_ts_ms: None,
129 is_agent_pane: true,
130 }
131 }
132
133 fn publish_status(&self) {
134 if let Some(ref broker) = self.broker {
135 let status = self.get_status_snapshot();
136 super::publish_controller_status(broker, &status);
137 }
138 }
139
140 fn is_running(&self) -> bool {
141 let inner = self.inner.lock().unwrap();
142 inner.stdin_tx.is_some()
143 }
144
145 fn make_request(&self, method: &str, params: serde_json::Value) -> String {
147 let id = self.next_id();
148 serde_json::json!({
149 "jsonrpc": "2.0",
150 "id": id,
151 "method": method,
152 "params": params,
153 }).to_string()
154 }
155
156 fn make_notification(&self, method: &str, params: serde_json::Value) -> String {
158 serde_json::json!({
159 "jsonrpc": "2.0",
160 "method": method,
161 "params": params,
162 }).to_string()
163 }
164
165 pub fn send_message(&self, message: String, cli_command: String, cli_args: Vec<String>, working_dir: String, env_vars: HashMap<String, String>) -> Result<(), String> {
169 if !self.is_running() {
170 {
173 let mut inner = self.inner.lock().unwrap();
174 inner.pending_prompt = Some(message.clone());
175 }
176 self.health_monitor.set_active_turn(true);
177 return self.spawn_process(cli_command, cli_args, working_dir, env_vars);
178 }
179
180 let session_id = {
182 let inner = self.inner.lock().unwrap();
183 inner.session_id.clone().unwrap_or_default()
184 };
185
186 let req = self.make_request("session/prompt", serde_json::json!({
187 "sessionId": session_id,
188 "prompt": {
189 "type": "text",
190 "text": message,
191 }
192 }));
193
194 self.health_monitor.set_active_turn(true);
195
196 let inner = self.inner.lock().unwrap();
197 let tx = inner.stdin_tx.as_ref()
198 .ok_or("ACP process not running after spawn")?;
199 tx.try_send(req)
200 .map_err(|e| format!("ACP stdin send failed: {e}"))
201 }
202
203 fn spawn_process(&self, cli_command: String, cli_args: Vec<String>, working_dir: String, env_vars: HashMap<String, String>) -> Result<(), String> {
205 let mut cmd = crate::server::cli_handlers::make_cli_cmd(&cli_command);
206 cmd.args(&cli_args);
207
208 core::apply_working_dir(&mut cmd, &self.block_id, &working_dir, &env_vars);
209 #[cfg(windows)]
213 {
214 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
215 cmd.creation_flags(CREATE_NO_WINDOW);
216 }
217
218 cmd.stdin(std::process::Stdio::piped());
219 cmd.stdout(std::process::Stdio::piped());
220 cmd.stderr(std::process::Stdio::piped());
221
222 let mut child = cmd.spawn().map_err(|e| {
223 tracing::error!(block_id = %self.block_id, error = %e, "ACP process spawn failed");
224 format!("failed to spawn ACP process: {e}")
225 })?;
226
227 let pid = child.id().unwrap_or(0);
228
229 tracing::info!(
230 block_id = %self.block_id,
231 pid = pid,
232 cmd = %cli_command,
233 args = ?cli_args,
234 "ACP agent process spawned"
235 );
236
237 let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<bool>();
238 let stdin = child.stdin.take()
239 .ok_or_else(|| format!("[acp] stdin not captured for block {}", self.block_id))?;
240 let stdout = child.stdout.take()
241 .ok_or_else(|| format!("[acp] stdout not captured for block {}", self.block_id))?;
242 let stderr = child.stderr.take();
243
244 if let Some(stderr_pipe) = stderr {
246 let block_id_stderr = self.block_id.clone();
247 tokio::spawn(async move {
248 let mut reader = BufReader::new(stderr_pipe).lines();
249 loop {
250 match reader.next_line().await {
251 Err(e) => {
252 tracing::warn!(block_id = %block_id_stderr, error = %e, "ACP stderr read error");
253 break;
254 }
255 Ok(None) => break,
256 Ok(Some(line)) => {
257 if !line.trim().is_empty() {
258 tracing::warn!(
259 block_id = %block_id_stderr,
260 line = %line,
261 "ACP agent stderr"
262 );
263 }
264 }
265 }
266 }
267 });
268 }
269
270 let (msg_tx, mut msg_rx) = mpsc::channel::<String>(32);
272
273 {
274 let mut inner = self.inner.lock().unwrap();
275 inner.current_pid = Some(pid);
276 inner.kill_tx = Some(kill_tx);
277 inner.stdin_tx = Some(msg_tx.clone());
278 Self::set_status(&mut inner, STATUS_RUNNING);
279 }
280 self.publish_status();
281
282 let block_id_stdin = self.block_id.clone();
284 tokio::spawn(async move {
285 let mut stdin = tokio::io::BufWriter::new(stdin);
286 while let Some(line) = msg_rx.recv().await {
287 if let Err(e) = stdin.write_all(line.as_bytes()).await {
288 tracing::error!(block_id = %block_id_stdin, error = %e, "ACP stdin write error");
289 break;
290 }
291 if let Err(e) = stdin.write_all(b"\n").await {
292 tracing::error!(block_id = %block_id_stdin, error = %e, "ACP stdin newline error");
293 break;
294 }
295 if let Err(e) = stdin.flush().await {
296 tracing::error!(block_id = %block_id_stdin, error = %e, "ACP stdin flush error");
297 break;
298 }
299 }
300 });
301
302 let block_id_stdout = self.block_id.clone();
304 let broker_clone = self.broker.clone();
305 let filestore_clone = self.filestore.clone();
306 let inner_clone = self.inner.clone();
307 let health_clone = self.health_monitor.clone();
308 let rpc_id_clone = self.next_rpc_id.clone();
309 let wstore_clone = self.wstore.clone();
310 let event_bus_clone = self.event_bus.clone();
311 let global_output_zone =
313 super::shell::resolve_global_output_zone(&self.wstore, &self.block_id);
314 tokio::spawn(async move {
315 let mut reader = BufReader::new(stdout).lines();
316 tracing::info!(block_id = %block_id_stdout, "ACP stdout_reader started");
317
318 loop {
319 let line = match reader.next_line().await {
320 Err(e) => {
321 tracing::warn!(block_id = %block_id_stdout, error = %e, "ACP stdout read error");
322 break;
323 }
324 Ok(None) => {
325 tracing::info!(block_id = %block_id_stdout, "ACP stdout EOF");
326 break;
327 }
328 Ok(Some(l)) => l,
329 };
330 if line.is_empty() {
331 continue;
332 }
333
334 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) {
336 if let Some(result) = json.get("result") {
338 if let Some(sid) = result.get("sessionId").and_then(|v| v.as_str()) {
339 let sid_owned = sid.to_string();
340 {
341 let mut inner = inner_clone.lock().unwrap();
342 inner.session_id = Some(sid_owned.clone());
343 if let Some(prompt) = inner.pending_prompt.take() {
345 let id = rpc_id_clone.fetch_add(1, Ordering::Relaxed);
346 let req = serde_json::json!({
347 "jsonrpc": "2.0",
348 "id": id,
349 "method": "session/prompt",
350 "params": {
351 "sessionId": sid,
352 "prompt": { "type": "text", "text": prompt },
353 }
354 }).to_string();
355 if let Some(ref tx) = inner.stdin_tx {
356 if tx.try_send(req).is_err() {
357 tracing::warn!(block_id = %block_id_stdout, "[acp] session/prompt send failed — channel full or closed");
358 }
359 }
360 }
361 }
362 tracing::info!(
363 block_id = %block_id_stdout,
364 session_id = %sid_owned,
365 "ACP session established"
366 );
367 core::persist_session_id(&block_id_stdout, &sid_owned, &wstore_clone, &event_bus_clone);
372 }
373 }
374
375 if json.get("id").is_some() && json.get("result").is_some() {
377 if let Some(result) = json.get("result") {
379 if result.get("stopReason").is_some() {
380 health_clone.set_active_turn(false);
381 }
382 }
383 }
384 }
385
386 if let Some(ref broker) = broker_clone {
388 let line_with_newline = format!("{}\n", line);
389 super::shell::handle_append_block_file(
390 broker,
391 &block_id_stdout,
392 ACP_OUTPUT_SUBJECT,
393 line_with_newline.as_bytes(),
394 filestore_clone.as_ref(),
395 global_output_zone.as_deref(),
396 );
397 }
398 }
399 });
400
401 let block_id_wait = self.block_id.clone();
403 let inner_wait = self.inner.clone();
404 let broker_wait = self.broker.clone();
405 let health_wait = self.health_monitor.clone();
406 tokio::spawn(async move {
407 tokio::select! {
408 _ = kill_rx => {
409 let _ = child.kill().await;
410 tracing::info!(block_id = %block_id_wait, "ACP process killed");
411
412 let mut inner = inner_wait.lock().unwrap();
413 inner.stdin_tx = None;
414 inner.current_pid = None;
415 AcpController::set_status(&mut inner, STATUS_DONE);
416 drop(inner);
417
418 health_wait.set_active_turn(false);
419
420 if let Some(ref broker) = broker_wait {
421 let status = BlockControllerRuntimeStatus {
422 blockid: block_id_wait.clone(),
423 version: 0,
424 shellprocstatus: STATUS_DONE.to_string(),
425 shellprocconnname: "local".to_string(),
426 shellprocexitcode: -1,
427 spawn_ts_ms: None,
428 is_agent_pane: true,
429 };
430 super::publish_controller_status(broker, &status);
431 }
432 }
433 status = child.wait() => {
434 let exit_code = status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1);
435 tracing::info!(
436 block_id = %block_id_wait,
437 exit_code = exit_code,
438 "ACP process exited"
439 );
440 let mut inner = inner_wait.lock().unwrap();
441 inner.proc_exit_code = exit_code;
442 inner.stdin_tx = None;
443 AcpController::set_status(&mut inner, STATUS_DONE);
444 drop(inner);
445
446 health_wait.set_active_turn(false);
447
448 if let Some(ref broker) = broker_wait {
449 let status = BlockControllerRuntimeStatus {
450 blockid: block_id_wait.clone(),
451 version: 0,
452 shellprocstatus: STATUS_DONE.to_string(),
453 shellprocconnname: "local".to_string(),
454 shellprocexitcode: exit_code,
455 spawn_ts_ms: None,
456 is_agent_pane: true,
457 };
458 super::publish_controller_status(broker, &status);
459 }
460 }
461 }
462 });
463
464 let init_req = self.make_request("initialize", serde_json::json!({
466 "clientInfo": {
467 "name": "AgentMux",
468 "version": env!("CARGO_PKG_VERSION"),
469 },
470 "capabilities": {
471 "tools": true,
472 "fileAccess": true,
473 },
474 "workspaceRoots": [working_dir],
475 }));
476 let init_notification = self.make_notification("initialized", serde_json::json!({}));
477 let session_req = self.make_request("session/create", serde_json::json!({
478 "cwd": working_dir,
479 }));
480
481 let inner = self.inner.lock().unwrap();
483 if let Some(ref tx) = inner.stdin_tx {
484 for (label, msg) in [("initialize", init_req), ("initialized", init_notification), ("session/create", session_req)] {
485 if tx.try_send(msg).is_err() {
486 tracing::error!(block_id = %self.block_id, method = label, "[acp] handshake message dropped — channel full or closed; agent will not start");
487 }
488 }
489 }
490
491 Ok(())
492 }
493}
494
495impl Controller for AcpController {
496 fn start(
497 &self,
498 block_meta: super::super::obj::MetaMapType,
499 _rt_opts: Option<serde_json::Value>,
500 _force: bool,
501 ) -> Result<(), String> {
502 let cmd = super::super::obj::meta_get_string(&block_meta, super::META_KEY_CMD, "");
504 let cwd = super::super::obj::meta_get_string(&block_meta, super::META_KEY_CMD_CWD, "");
505 let args_str = super::super::obj::meta_get_string(&block_meta, super::META_KEY_CMD_ARGS, "[]");
506 let env_str = super::super::obj::meta_get_string(&block_meta, super::META_KEY_CMD_ENV, "{}");
507
508 if cmd.is_empty() {
509 return Err("ACP controller: no cmd specified in block meta".to_string());
510 }
511
512 let args: Vec<String> = serde_json::from_str(&args_str).unwrap_or_default();
513 let env_vars: HashMap<String, String> = serde_json::from_str(&env_str).unwrap_or_default();
514
515 self.spawn_process(cmd, args, cwd, env_vars)
516 }
517
518 fn stop(&self, _graceful: bool, _new_status: &str) -> Result<(), String> {
519 {
521 let inner = self.inner.lock().unwrap();
522 if let Some(ref tx) = inner.stdin_tx {
523 let shutdown = self.make_request("shutdown", serde_json::json!({}));
524 let exit = self.make_notification("exit", serde_json::json!({}));
525 for (label, msg) in [("shutdown", shutdown), ("exit", exit)] {
526 if tx.try_send(msg).is_err() {
527 tracing::debug!(block_id = %self.block_id, method = label, "[acp] shutdown message dropped — process likely already gone");
528 }
529 }
530 }
531 }
532
533 let kill_tx = {
535 let mut inner = self.inner.lock().unwrap();
536 inner.stdin_tx = None;
537 inner.kill_tx.take()
538 };
539 if let Some(tx) = kill_tx {
540 let _ = tx.send(true);
541 }
542
543 {
544 let mut inner = self.inner.lock().unwrap();
545 Self::set_status(&mut inner, STATUS_DONE);
546 }
547 self.publish_status();
548 Ok(())
549 }
550
551 fn get_runtime_status(&self) -> BlockControllerRuntimeStatus {
552 self.get_status_snapshot()
553 }
554
555 fn send_input(&self, input: BlockInputUnion, _seq: Option<u64>) -> Result<(), String> {
556 if let Some(data) = input.input_data {
557 let message = String::from_utf8_lossy(&data).to_string();
560 if message.trim().is_empty() {
561 return Ok(());
562 }
563
564 if !self.is_running() {
565 let mut inner = self.inner.lock().unwrap();
567 inner.pending_prompt = Some(message);
568 return Err("ACP process not running — message queued for next start()".to_string());
569 }
570
571 let session_id = {
572 let inner = self.inner.lock().unwrap();
573 inner.session_id.clone().unwrap_or_default()
574 };
575 let req = self.make_request("session/prompt", serde_json::json!({
576 "sessionId": session_id,
577 "prompt": {
578 "type": "text",
579 "text": message,
580 }
581 }));
582 self.health_monitor.set_active_turn(true);
583 let inner = self.inner.lock().unwrap();
584 if let Some(ref tx) = inner.stdin_tx {
585 tx.try_send(req)
586 .map_err(|e| format!("ACP stdin send failed: {e}"))?;
587 }
588 }
589
590 if let Some(sig) = input.sig_name {
591 if sig == "SIGTERM" || sig == "SIGINT" {
592 return self.stop(true, STATUS_DONE);
593 }
594 }
595
596 Ok(())
597 }
598
599 fn controller_type(&self) -> &str {
600 BLOCK_CONTROLLER_ACP
601 }
602
603 fn block_id(&self) -> &str {
604 &self.block_id
605 }
606
607 fn as_any(&self) -> &dyn std::any::Any {
608 self
609 }
610}